티스토리 뷰

Infra

쿠버네티스 오퍼레이터를 Java로 개발해보기

지마켓 기술블로그 2024. 7. 1. 00:00

이전 포스트: 쿠버네티스 오퍼레이터를 Golang으로 개발해보기

 

 

안녕하세요.

Cloud Strategy팀 박규민입니다.

 

 

지난번에 Golang으로 쿠버네티스 오퍼레이터를 간단하게 만들어 봤습니다. 하지만 국내에서는 아무래도 Golang보다는 Java의 수요가 압도적으로 많은데요. 이번 포스트로 Java로 오퍼레이터를 구현하는 과정을 보여드리겠습니다.

 

 

Java Operator SDK

Java Operator SDK는 Kubernetes Client Java API인 fabric8io 기반으로 작성되어 있습니다. 이는 세부적으로 쿠버네티스와 상호 작용하기 위한 Low Level 단에서의 코드 작성 걱정 없이 개발자에게 친숙한 Java API를 사용하여 오퍼레이터를 쉽게 작성할 수 있도록 설계되어 있습니다.

 

 

 

Architecture

Operator Controller 클래스의 집합이며, Controller 클래스가 Kubernetes 단일 리소스를 조정(Reconciling)해주는 역할을 합니다.

EventSourceManager가 Controller와 관련된 여러 EventSource들의 수명 주기를 관리해 줍니다. 여기에서의 Event는 리소스 조정을 유발하는 사건을 의미합니다.

 

EventSource에서 EventProcessor에 전파되는 Event를 생성합니다. (Controller와 관련된 기본 리소스의 변경 사항을 감시할 때는 ControllerResourceEventSource를 통해 Event를 전파하여 관련 상태를 캐싱합니다) Event를 받은 EventProcessor에서 리소스가 아직 처리되지 않은 경우, 적절한 Reconciler 메소드로 호출하여 전달해 주는 ReconcilerDispatcher를 호출하여 필요한 모든 정보를 차례대로 전달합니다.

 

Reconciler 메소드가 끝났을 때 EventProcessor가 다시 호출되어 실행을 완료하고 Controller의 상태를 업데이트합니다. 그리고 Reconciler 메소드에서 반환한 결과에 따라서 필요한 경우, ReconcilerDispatcher는 Kubernetes API 서버에 호출합니다.

마지막으로 EventProcessor는 요청 재시도를 해야 할지, 그리고 동일한 리소스에 대해 수신된 후속 Event가 없는지 확인합니다. 이 중 어느 것도 일어나지 않으면, Event 처리가 완료됩니다.

 

 

직접 구현해야 할 포인트는?

개발자들에게는 다음과 같은 구성 요소들을 Java Class로 만들 필요가 있습니다.

  • Primary Resource: k8s 클러스터에 배포할 CRD
  • Spec, Status: CRD에 필요한 내부 구성 요소
    • Spec: 사용자가 CR에 적용할 상태 정의
    • Status: CR의 현재 상태
  • Reconciler: Primary Resource의 변경사항을 감지하고 조정
  • KubernetesDependentResource: CRD 배포의 결과로 클러스터에서 만들고 싶은 각 K8s 리소스(DeploymentConfig, Service, Ingress 등)

 

Project 생성

먼저 Spring Boot 기반으로 프로젝트를 생성합니다.

Intellij 기준으로 Spring Initializr Generators 메뉴를 통해 프로젝트를 생성합니다.

 

 

 

 

build.gradle에서 다음과 같이 수정합니다.

plugins {
  id 'java'
  id 'org.springframework.boot' version '3.3.1'
  id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(21)
  }
}

repositories {
  mavenCentral()
}

dependencies {
  annotationProcessor 'io.fabric8:crd-generator-apt:6.13.0'

  implementation 'org.springframework.boot:spring-boot-starter'
  implementation 'io.javaoperatorsdk:operator-framework-spring-boot-starter:5.5.0'

  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'io.javaoperatorsdk:operator-framework-spring-boot-starter-test:5.5.0'
  testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
  useJUnitPlatform()
}

 

 

 

Custom Resource 작성

customresources라는 package를 만든 다음, 하위에 Spec, Status, Primary Resource Class를 만듭니다.

Spec Class

package com.example.petclinicoperatorjava.customresources;

// Lombok @Data로도 적용 가능
public class PetclinicSpec {
  private String image;
  private Integer size;

  public String getImage() {
    return image;
  }

  public void setImage(String image) {
    this.image = image;
  }

  public Integer getSize() {
    return size;
  }

  public void setSize(Integer size) {
    this.size = size;
  }

  public Integer getPort() {
    return port;
  }

  public void setPort(Integer port) {
    this.port = port;
  }

  private Integer port;
}

 

Status Class

ObservedGenerationAwareStatus 클래스를 확장합니다.

이는 k8s의 controller가 Petclinic CR에 대한 변경사항을 추적할 수 있도록 Petclinic 오브젝트가 매번 변경될 때마다 Petclinic CR 내에 observedGeneration status 값을 증가시킵니다.

package com.example.petclinicoperatorjava.customresources;

import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;

public class PetclinicStatus extends ObservedGenerationAwareStatus {
}

Primary Resource Class

Petclinic CR을 Class로 생성합니다.

CustomResource 추상클래스를 확장할 때 PetclinicSpec과 PetclinicStatus 클래스를 참조합니다.

 

package com.example.petclinicoperatorjava.customresources;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("spring.my.domain")
@Version("v1")
public class Petclinic extends CustomResource<PetclinicSpec, PetclinicStatus> implements Namespaced {

  @Override
  public String toString() {
    return "Petclinic{spec=" + spec + ", status=" + status + "}";
  }
}

 

 

 

Dependent Resources 작성

dependentresources라는 package를 생성한 다음, 하위에 KubernetesDependentResource 클래스들을 만듭니다.

CRUDKubernetesDependentResource 추상클래스를 확장하여 CRD에 필요한 k8s 리소스들의 manifest를 class로 정의합니다.

@KubernetesDependent 어노테이션을 통해 Petclinic CR의 변화에 대응하여 해당 k8s 리소스의 수명 주기를 관리합니다.

Deployment

package com.example.petclinicoperatorjava.dependentresources;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class PetclinicDeploymentResource extends CRUDKubernetesDependentResource<Deployment, Petclinic> {

  public PetclinicDeploymentResource() {
    super(Deployment.class);
  }

  @Override
  protected Deployment desired(Petclinic petclinic, Context<Petclinic> context) {
    final ObjectMeta petclinicMetadata = petclinic.getMetadata();
    final String petclinicName = petclinicMetadata.getName();

    return new DeploymentBuilder()
        .editMetadata()
          .withName(petclinicName)
          .withNamespace(petclinicMetadata.getNamespace())
          .addToLabels("app", petclinicName)
          .endMetadata()
        .editSpec()
          .withSelector(new LabelSelectorBuilder()
            .addToMatchLabels("app", petclinicName)
            .build())
          .withReplicas(petclinic.getSpec().getSize())
          .withTemplate(new PodTemplateSpecBuilder()
            .editMetadata()
            .addToLabels("app", petclinicName)
            .endMetadata()
            .editSpec()
            .withContainers(new ContainerBuilder()
                .withName(petclinicName + "-container")
                .withImage(petclinic.getSpec().getImage())
                .addToPorts(new ContainerPortBuilder()
                  .withContainerPort(petclinic.getSpec().getPort())
                  .build())
                .build())
            .endSpec()
            .build())
        .endSpec()
        .build();
  }
}

Service

package com.example.petclinicoperatorjava.dependentresources;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import io.fabric8.kubernetes.api.model.*;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class PetclinicServiceResource extends CRUDKubernetesDependentResource<Service, Petclinic> {

  public PetclinicServiceResource() {
    super(Service.class);
  }

  @Override
  protected Service desired(Petclinic petclinic, Context<Petclinic> context) {
    final ObjectMeta petclinicMetadata = petclinic.getMetadata();
    final String petclinicName = petclinicMetadata.getName();

    return new ServiceBuilder()
        .editMetadata()
          .withName(petclinicName)
          .withNamespace(petclinicMetadata.getNamespace())
          .addToLabels("app", petclinicName)
        .endMetadata()
        .editSpec()
          .withType("NodePort")
          .addToSelector("app", petclinicName)
          .addToPorts(new ServicePortBuilder().withName("http").withPort(petclinic.getSpec().getPort()).withProtocol("TCP").withTargetPort(new IntOrStringBuilder().withValue(petclinic.getSpec().getPort()).build()).build())
        .endSpec()
        .build();
  }
}

Ingress

package com.example.petclinicoperatorjava.dependentresources;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.networking.v1.*;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class PetclinicIngressResource extends CRUDKubernetesDependentResource<Ingress, Petclinic> {

  public PetclinicIngressResource() {
    super(Ingress.class);
  }

  @Override
  protected Ingress desired(Petclinic petclinic, Context<Petclinic> context) {
    final ObjectMeta petclinicMetadata = petclinic.getMetadata();
    final String petclinicName = petclinicMetadata.getName();

    return new IngressBuilder()
        .editMetadata()
          .withName(petclinicName)
          .withNamespace(petclinicMetadata.getNamespace())
          .addToLabels("app", petclinicName)
        .endMetadata()
        .editSpec()
          .withIngressClassName("nginx")
          .withRules(new IngressRuleBuilder()
            .withHttp(new HTTPIngressRuleValueBuilder()
              .withPaths(new HTTPIngressPathBuilder()
                .withPath("/")
                .withPathType("Prefix")
                .withBackend(new IngressBackendBuilder()
                  .withService(new IngressServiceBackendBuilder()
                    .withName(petclinicName)
                    .withPort(new ServiceBackendPortBuilder()
                      .withNumber(petclinic.getSpec().getPort())
                      .build())
                    .build())
                  .build())
                .build())
              .build())
            .build())
        .endSpec()
        .build();
  }
}

 

 

 

Reconciler 작성

Petclinic Primary Resource의 변경사항을 감지하고 조정을 해주는 Reconciler 클래스를 작성합니다.

@ControllerConfiguration 어노테이션을 붙여 dependents 속성을 통해 @KubernetesDependent 어노테이션을 붙인 KubernetesDependentResource 클래스를 @Dependent 어노테이션으로 여기에 연결합니다.

package com.example.petclinicoperatorjava;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import com.example.petclinicoperatorjava.dependentresources.PetclinicDeploymentResource;
import com.example.petclinicoperatorjava.dependentresources.PetclinicIngressResource;
import com.example.petclinicoperatorjava.dependentresources.PetclinicServiceResource;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import org.springframework.stereotype.Component;

@ControllerConfiguration(
    dependents = {
        @Dependent(type = PetclinicDeploymentResource.class),
        @Dependent(type = PetclinicServiceResource.class),
        @Dependent(type = PetclinicIngressResource.class)
    })
public class PetclinicReconciler implements Reconciler<Petclinic>, ErrorStatusHandler<Petclinic>, Cleaner<Petclinic> {

  @Override
  public UpdateControl<Petclinic> reconcile(Petclinic petclinic, Context<Petclinic> context) {
    return UpdateControl.updateResourceAndPatchStatus(petclinic);
  }

  @Override
  public DeleteControl cleanup(Petclinic petclinic, Context<Petclinic> context) {
    return DeleteControl.defaultDelete();
  }

  @Override
  public ErrorStatusUpdateControl<Petclinic> updateErrorStatus(Petclinic petclinic, Context<Petclinic> context, Exception e) {
    return ErrorStatusUpdateControl.patchStatus(petclinic);
  }

}

 

 

 

Config 작성

해당 Reconciler 클래스와 직접 구현한 PetclinicOperator를 각각 Bean으로 등록하도록 Config 클래스를 config package 하위에 만듭니다.

package com.example.petclinicoperatorjava.config;

import com.example.petclinicoperatorjava.PetclinicReconciler;
import io.javaoperatorsdk.operator.Operator;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class PetclinicOperatorConfig {

  @Bean
  public PetclinicReconciler petclinicReconciler() {
    return new PetclinicReconciler();
  }

  @Bean(initMethod = "start", destroyMethod = "stop")
  public Operator operator(List<Reconciler<?>> controllers) {
    Operator operator = new Operator();
    controllers.forEach(operator::register);
    return operator;
  }
}

 

 

 

k8s 클러스터에 CRD 적용

io.fabric8:crd-generator-apt 라이브러리에 의해 프로젝트 컴파일을 진행할 시 자동으로 build 디렉토리 내부에서 classpath/META-INF/fabric8 디렉토리에 yml 형식으로 생성됩니다.

이를 지난 시간에 구축한 kind 기반의 로컬 k8s 클러스터에 CRD로 등록합니다.

 

$ kubectl apply -f ./build/classes/java/main/META-INF/fabric8/petclinics.spring.my.domain-v1.yml
customresourcedefinition.apiextensions.k8s.io/petclinics.spring.my.domain created

 

 

 

로컬 테스트

PetclinicOperatorJavaApplication에서 main 클래스를 실행합니다.

 

kind cluster에 Petclinic manifest가 적용이 잘 되는지 확인합니다.

$ kubectl config set-context --current --namespace=petclinic

$ kubectl apply -f - <<EOF
apiVersion: spring.my.domain/v1
kind: Petclinic
metadata:
  name: sample
spec:
  image: springio/petclinic
  size: 1
  port: 8080
EOF
petclinic.spring.my.domain/sample created

$ kubectl get all
NAME                          READY   STATUS    RESTARTS   AGE
pod/sample-594c8976df-5gmzr   1/1     Running   0          47s

NAME             TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/sample   NodePort   10.96.34.237   <none>        8080:32494/TCP   48s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample   1/1     1            1           48s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-594c8976df   1         1         1       48s

$ kubectl get ing
NAME     CLASS   HOSTS   ADDRESS     PORTS   AGE
sample   nginx   *       localhost   80      69s

 

http://localhost로 접속하여 Petclinic 메인 화면이 나오는지 확인합니다.

 

 

 

 

Integration Test

@EnableMockOperator 어노테이션을 통해 k8s 클러스터를 mocking 하여 직접 구현한 Operator의 CRD를 적용하여 전용 통합 테스트를 작성할 수 있습니다.

package com.example.petclinicoperatorjava;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.springboot.starter.test.EnableMockOperator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@EnableMockOperator(crdPaths = "classpath:META-INF/fabric8/petclinics.spring.my.domain-v1.yml")
public class PetclinicOperatorUnitTest {

  @Autowired
  KubernetesClient k8sClient;

  @Test
  void whenContextLoaded_thenCrdApplied() {
    assertThat(
        k8sClient
            .apiextensions()
            .v1()
            .customResourceDefinitions()
            .withName("petclinics.spring.my.domain")
            .get()
    ).isNotNull();
  }
}

 

 

 

Operator 배포

먼저 gradle build task를 실행하여 jar 파일을 생성합니다.

 

 

프로젝트 루트 프로젝트 밑에 k8s라는 디렉토리를 만들어 다음과 같이 Dockerfile(./k8s/Dockerfile)을 정의합니다.

FROM bellsoft/liberica-openjdk-alpine:21

COPY build/libs/*-0.0.1-SNAPSHOT.jar /opt/app/app.jar

EXPOSE 80

CMD ["java", "-showversion", "-jar", "/opt/app/app.jar"]

 

 

프로젝트 루트 디렉토리에서 docker build 명령어를 실행하여 petclinic java operator 이미지를 생성합니다.

# 개인 docker public registry에 petclinic java operator를 push합니다.
# 현재 로컬 kind cluster에서의 worker node는 linux/arm64 기반이어서 맞출 필요가 있음
$ docker build -f ./k8s/Dockerfile -t ycatt/petclinic-operator-java:0.0.1 . --platform "linux/arm64"

$ docker login

$ docker push ycatt/petclinic-operator-java:0.0.1

 

kind 로컬 클러스터에 다음과 같은 매니페스트 파일을 배포합니다. (./k8s/petclinic-operator.yaml)

apiVersion: v1
kind: ServiceAccount
metadata:
  name: petclinic-operator

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: petclinic-operator
spec:
  selector:
    matchLabels:
      app: petclinic-operator
  replicas: 1
  template:
    metadata:
      labels:
        app: petclinic-operator
    spec:
      serviceAccountName: petclinic-operator
      containers:
        - name: petclinic-operator
          image: ycatt/petclinic-operator-java:0.0.1
          imagePullPolicy: Always
          ports:
            - containerPort: 80

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: petclinic-operator-admin
subjects:
  - kind: ServiceAccount
    name: petclinic-operator
    namespace: default
roleRef:
  kind: ClusterRole
  name: petclinic-operator
  apiGroup: ""

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: petclinic-operator
rules:
  - apiGroups:
      - ""
      - "extensions"
      - "apps"
    resources:
      - deployments
      - services
      - pods
      - pods/exec
    verbs:
      - '*'
  - apiGroups:
      - "apiextensions.k8s.io"
    resources:
      - customresourcedefinitions
    verbs:
      - '*'
  - apiGroups:
      - "spring.my.domain"
    resources:
      - petclinics
    verbs:
      - '*'
  - apiGroups:
      - "networking.k8s.io"
    resources:
      - ingresses
    verbs:
      - '*'
$ kubectl apply -f ./k8s/petclinic-operator.yaml
serviceaccount/petclinic-operator created
deployment.apps/petclinic-operator created
clusterrolebinding.rbac.authorization.k8s.io/petclinic-operator-admin created
clusterrole.rbac.authorization.k8s.io/petclinic-operator created

$ kubectl get pod -n default
NAME                                  READY   STATUS    RESTARTS   AGE
petclinic-operator-6dc9f97b96-qg84p   1/1     Running   0          20m

$ kubectl logs petclinic-operator-6dc9f97b96-qg84p -n default
openjdk version "21.0.3" 2024-04-16 LTS
OpenJDK Runtime Environment (build 21.0.3+10-LTS)
OpenJDK 64-Bit Server VM (build 21.0.3+10-LTS, mixed mode)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.1)

2024-07-02T06:57:28.720Z  INFO 1 --- [petclinic-operator-java] [           main] c.e.p.PetclinicOperatorJavaApplication   : Starting PetclinicOperatorJavaApplication v0.0.1-SNAPSHOT using Java 21.0.3 with PID 1 (/opt/app/app.jar started by root in /)
2024-07-02T06:57:28.723Z  INFO 1 --- [petclinic-operator-java] [           main] c.e.p.PetclinicOperatorJavaApplication   : No active profile set, falling back to 1 default profile: "default"
2024-07-02T06:57:29.603Z  WARN 1 --- [petclinic-operator-java] [           main] ault ConfigurationService implementation : Configuration for reconciler 'petclinicreconciler' was not found. Known reconcilers: None.
2024-07-02T06:57:29.619Z  INFO 1 --- [petclinic-operator-java] [           main] ault ConfigurationService implementation : Created configuration for reconciler com.example.petclinicoperatorjava.PetclinicReconciler with name petclinicreconciler
2024-07-02T06:57:29.657Z  INFO 1 --- [petclinic-operator-java] [           main] io.javaoperatorsdk.operator.Operator     : Registered reconciler: 'petclinicreconciler' for resource: 'class com.example.petclinicoperatorjava.customresources.Petclinic' for namespace(s): [all namespaces]
2024-07-02T06:57:29.659Z  INFO 1 --- [petclinic-operator-java] [           main] io.javaoperatorsdk.operator.Operator     : Operator SDK 4.9.1 (commit: 135b239) built on Tue May 28 08:09:11 GMT 2024 starting...
2024-07-02T06:57:29.666Z  INFO 1 --- [petclinic-operator-java] [           main] io.javaoperatorsdk.operator.Operator     : Client version: 6.12.1
2024-07-02T06:57:29.667Z  INFO 1 --- [petclinic-operator-java] [linicreconciler] i.j.operator.processing.Controller       : Starting 'petclinicreconciler' controller for reconciler: com.example.petclinicoperatorjava.PetclinicReconciler, resource: com.example.petclinicoperatorjava.customresources.Petclinic
2024-07-02T06:57:30.233Z  INFO 1 --- [petclinic-operator-java] [linicreconciler] i.j.operator.processing.Controller       : 'petclinicreconciler' controller started
2024-07-02T06:57:30.360Z  INFO 1 --- [petclinic-operator-java] [           main] c.e.p.PetclinicOperatorJavaApplication   : Started PetclinicOperatorJavaApplication in 1.885 seconds (process running for 2.43)

 

 

IDE에서 실행한 Petclinic Operator Application을 Terminate한 뒤, 기존에 로컬에서 테스트했던 Petclinic CR을 삭제하고 재생성하여 정상적으로 메인 페이지 접속이 되는 지 확인합니다.

$ kubectl delete petclinic --all
petclinic.spring.my.domain "sample" deleted

$ kubectl apply -f - <<EOF
apiVersion: spring.my.domain/v1
kind: Petclinic
metadata:
  name: sample
  namespace: petclinic
spec:
  image: springio/petclinic
  size: 1
  port: 8080
EOF
petclinic.spring.my.domain/sample created

$ kubectl get all -n petclinic
NAME                          READY   STATUS    RESTARTS   AGE
pod/sample-594c8976df-k4944   1/1     Running   0          8s

NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/sample   NodePort   10.96.140.108   <none>        8080:30502/TCP   8s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample   1/1     1            1           8s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-594c8976df   1         1         1       8s

$ kubectl get ing -n petclinic
NAME     CLASS   HOSTS   ADDRESS     PORTS   AGE
sample   nginx   *       localhost   80      44s

# curl로 확인
$ curl http://localhost:80 -v -o /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200
< Date: Tue, 02 Jul 2024 07:31:39 GMT
< Content-Type: text/html;charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Content-Language: en-US
<
{ [3168 bytes data]
100  3161    0  3161    0     0  10577      0 --:--:-- --:--:-- --:--:-- 10788
* Connection #0 to host localhost left intact

 

 

 

마치면서...

이번 포스트에서는 Java를 사용하여 Kubernetes Operator를 직접 구현해 보았습니다. Golang에 비해 구조가 비교적 간단하고 친숙하여 더 쉽게 접근할 수 있다는 느낌이 있는데요. 기존 Java/Spring Boot 기반 서비스와의 연계성, 개발자 커뮤니티의 폭넓은 지원까지 생각해 본다면 Kubernetes Operator를 개발할 때 Java라는 언어로도 꽤나 매력적인 선택이 될 수 있습니다. Java Operator SDK 활용에 작은 도움이 되었기를 바라면서 글을 마치겠습니다.

 

감사합니다.

 

 

 

참조

 

댓글