자바 애플리케이션에서 k8s를 제어하는 보기 드문 일을 하게 되었다… (내가 이해하기로) Spring Cloud Kuberenetes의 경우 Spring 기반 MSA를 k8s에 배포하는데 사용하지만, k8s 자체를 java로 제어하고 인터렉션하기 위해서는 fabric8, k8s java client(이하 java client) 등을 이용해야 한다(그렇기 때문에 ‘Spring’ cloud이고 ‘java’ client인 것 같기도). 나의 경우 홈서버에 띄워져있는 k8s 인프라의 일부를 제어하는 Spring Application을 만들어야 하기 때문에 후자가 필요했다.
fabric8 vs k8s java client
그렇다면 fabric8와 official java client 중에 어떤 걸 선택해야 하느냐. 2024년 8월 기준 java client의 fork와 star 수가 조금 더 많다. 업데이트는 둘 다 꾸준히 되는 것 같고.
fabric8가 조금 더 오래된 라이브러리다보니 인터넷에 예제 등이 조금 더 풍부한 편인 것 같았다. 공식 Docs도 조금 더 잘 되어 있고 무엇보다 ‘java 개발자’가 쉽게 사용하기 편하게 설계 되어 있다는 느낌이 강했다. 예제도 많고 빠르게 개발하고 싶다면 개인적으로 fabric8를 추천할 것 같다. 또한 온프레미스 k8s뿐만 아니라 Openshift 클러스터의 clinet를 만드는데도 사용할 수 있고, istio, chaosMesh 등에 대한 여러 extension을 제공해 주어진 문제를 빠르게 풀 때 유용하다.
PodList podList = client.pods().inNamespace(namespace).list(new ListOptionsBuilder().withLimit(5L).build());
podList.getItems().forEach(obj -> System.out.println(obj.getMetadata().getName()));
podList = client.pods().inNamespace(namespace)
.list(new ListOptionsBuilder().withLimit(5L).withContinue(podList.getMetadata().getContinue()).build());
podList.getItems().forEach(obj -> System.out.println(obj.getMetadata().getName()));
java client는 인터넷에 자료도 상대적으로 얼마 없고 docs가 뭔가 풍부하지가 않다(javadoc이 구리다는 얘기는 아님)… 뒤에서 얘기하겠지만 개발자 편의성 개선을 엄청했음에도 이를 안 알리는 느낌이 강하다. 만약 아래와 같은 null 지옥 예시를 봤다면 걱정말아라(LLM도 저렇게 알려주더라;;) 처음에 아래 예시보고 java client 사용을 조금 망설였다.
CoreV1Api api = new CoreV1Api();
V1PodList list =
api.listPodForAllNamespaces(null,
null,
null,
null,
100,
null,
null,
null,
null,
null);
list.getItems().stream()
.map(V1Pod::getMetadata)
.map(V1ObjectMeta::getName)
.forEach(System.out::println);
java client의 가장 매력적인 점은 api 설계가 k8s api 설계랑 거의 비슷하다는 점이다(물론 fabric8도 비슷하지만 이쪽이 공식인만큼 좀 더 거기에 노력을 기울이는 느낌). 원하는 기능 예시를 잘 못찾길래 우연히 javadoc을 좀 들여다봤는데 내부 클래스 구조랑 k8s api들이랑 유사하다는 느낌을 많이 받았다. k8s에 대해 조금 더 깊이 이해해보고 싶다면(그리고 어짜피 kubectl이나 k8s 내부 동작도 api 호출 방식이니) java client가 조금 더 좋아보였다.
두 문서를 같이 보면 빠르게 비교가 가능하다.
java client 문서 - https://javadoc.io/doc/io.kubernetes/client-java-api/latest/io/kubernetes/client/openapi/models/V1Pod.html
k8s api 문서 - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#pod-v1-core
그리고 아주 좋은 것은 kubectl이랑 아주아주 비슷한 명령어를 제공한다(처음엔 여기에 영업당했다가 이후 API 설계에 영업당함)
// kubectl get -n default pod foo
V1Pod pod = Kubectl.get(V1Pod.class)
.namespace("default")
.name("foo")
.execute();
// kubectl patch --type='strategic' --patch="{\\"metadata\\":{\\"labels\\":{\\"foo\\":\\"bar\\"}}"
V1Pod patchedPod = Kubectl.patch(V1Pod.class)
.patchType(V1Patch.PATCH_FORMAT_STRATEGIC_MERGE_PATCH)
.patchContent(new V1Patch("{\\"metadata\\":{\\"labels\\":{\\"foo\\":\\"bar\\"}}"))
.execute();
여튼 위와 같은 점들이 마음에 들어 java client를 쓰기로 했다.
java client 사용하기
우선 의존성부터 설정하기. kubectl 유사 문법을 사용하고 싶다면 extended 버전을 넣으면 된다(처음에 썼다가 나중에 안 쓰게 되었지만 남겨두었다). 버전은 24년 7, 8월 기준 최신인 21버전으로 선택
implementation 'io.kubernetes:client-java-extended:21.0.0'
configuration 설정을 해야한다. java client 역시 .kube/config 내용을 바탕으로 k8s와 통신하기 때문에 이에 대한 정보를 주는 작업이 필요하다. coreV1Api는 apiClient에 대한 정보가 필요하므로 미리 설정을 해서 bean 등록을 해둔다. (@Configuration이랑 client.openapi.Configuration의 Configuration이라 이름 겹쳐서 후자를 풀로 적어야 한다 🙃)
@Configuration
public class KubeClientConfig {
@Value("${kube.configPath}")
private String configPath;
@Bean
public ApiClient apiClient() throws IOException {
String kubeConfigPath = System.getenv("HOME") + configPath;
ApiClient client = ClientBuilder
.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath)))
.build();
io.kubernetes.client.openapi.Configuration.setDefaultApiClient(client);
return client;
}
@Bean
public CoreV1Api coreV1Api(ApiClient apiClient) {
return new CoreV1Api(apiClient);
}
}
이외의 작업이야 docs를 참고하여 개발하면 된다. 나의 경우 아래와 같이 사용했다. label을 바탕으로 svc를 찾는 기능, svc와 그 svc가 들고 있는 pod의 podStatus를 조회하여 하나의 DTO로 묶는 기능 등을 구현하였다
@Slf4j
@Service
@RequiredArgsConstructor
public class KubeExecutor {
private final CoreV1Api coreV1Api;
public List<ServiceMetadata> getServiceMetadataOf(String projectName) {
try {
List<V1Service> services = coreV1Api
.listNamespacedService(namespace)
.labelSelector("projectName" + "=" + projectName)
.execute().getItems();
return services.stream()
.map(service -> ServiceMetadata.of(service, getServiceStateFrom(service)))
.toList();
} catch (ApiException | NullPointerException e) {
log.error(e.getMessage());
throw new KubeException();
}
}
// Service 당 Application 실행을 위한 Pod 한개, Pod 1개에 Container 1개로 전제
public ServiceState getServiceStateFrom(V1Service service) {
try {
String labelSelector = getLabelSelector(service);
V1Pod pod = coreV1Api.listNamespacedPod(namespace)
.labelSelector(labelSelector)
.execute()
.getItems()
.get(0);
return ServiceState.from(pod.getStatus().getContainerStatuses().get(0));
} catch (ApiException | NullPointerException e) {
log.error(e.getMessage());
throw new KubeException();
}
}
public String getLabelSelector(V1Service service) {
try{
return service.getSpec().getSelector().entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining(","));
} catch (NullPointerException e) {
log.error(e.getMessage());
throw new KubeException();
}
}
}
coreV1Api의 경우 k8s와 통신 도중 에러가 발생하면 ApiException을 던지는데, 나는 이를 catch하여 커스텀 exception을 던지도록 하였다. V1 객체 내부의 값을 getter로 꺼내면서 NullPointerException이 날 가능성이 항상 존재하게 되는데(이건 좀 확실히 불편하다. 모두 객체로 감싸져 있어 null safe 하지 않은 환경에 노출될 가능성이 좀 있어보였다) 중 리소스를 조회하는 도중에 NPE가 발생해도 catch하여 커스텀 exception을 던지도록 하였다(custom exception을 통해 에러 메시지를 한 곳에서 관리하고, 동일한 톤의 메시지가 나가도록 하였다).
'프로젝트 일지 > BoWS' 카테고리의 다른 글
Synology NAS NFS 기반 PV 생성하기 (0) | 2024.08.17 |
---|---|
자체 제작 KaaS에서 배포된 애플리케이션의 상태 보여주기 (0) | 2024.08.12 |
Springboot 애플리케이션을 통해 helm 배포해보기 (feat. ProcessBuilder) (0) | 2024.08.01 |
Helm으로 애플리케이션 배포 한 방에 해보기 (2) | 2024.07.22 |
Springboot - React - MySQL 애플리케이션 k8s에 배포하기 (0) | 2024.07.18 |