들어가며
쿠버네티스를 운영하다 보면 자연스럽게 이런 생각이 들 때가 있습니다. “지금 클러스터에서 Pod들이 정상적으로 작동하고 있는지, 각 Pod의 상태를 한 눈에 수집할 수 있으면 좋겠다.” 처음에는 단순한 호기심처럼 시작되는 이 질문이, 운영하는 서버 수가 늘어나기 시작하면 매일같이 반복되는 운영자의 고민으로 바뀝니다.
이런 요구를 해결해주는 것이 바로 Kubernetes 클라이언트 라이브러리였습니다. Spring Boot 애플리케이션 안에서 쿠버네티스 클러스터의 상태를 직접 조회하고, 변화에 실시간으로 반응할 수 있게 해주는 라이브러리입니다. 단순히 “외부에서 클러스터를 들여다보는” 도구라기보다는, 애플리케이션 자체가 클러스터의 일부가 되어 함께 호흡하게 만들어주는 다리 역할에 가깝습니다.
이 글에서는 제가 실제 프로젝트에서 Spring Boot에 Kubernetes 클라이언트를 도입하면서 정리한 도입 동기, 동작 원리, 그리고 운영 환경에서 반드시 짚고 넘어가야 했던 보안적 주의사항을 차례대로 풀어보려 합니다.
도입 동기
이 기술을 찾아보게 된 계기는 한 줄로 요약하면 운영 자동화와 서버 상태 수집의 필요성이었습니다. 현재 참여하고 있는 프로젝트는 100개 이상의 서버가 실행 중이고, 앞으로 더 늘어날 예정입니다. 단순히 숫자가 많아진다는 의미를 넘어, 운영자 한 명이 모든 서버의 상태를 머릿속에 그려두는 것 자체가 불가능한 단계로 접어들었다는 뜻이기도 합니다.
기존 방식으로는 클러스터 상태를 확인하려면 직접 Shell에 접속해 kubectl 명령어를 실행하거나, ArgoCD에 따로 접속해야 하는 번거로움이 있었습니다. kubectl get pods 부터 시작하는 과정 — 이 과정 자체가 장애 대응 시간을 갉아먹는 요인이라는 생각이 점점 커졌습니다.
기존 GitOps 환경의 한계
저는 Spring Boot에서 JGit을 통해 GitOps 환경을 수정하고, ArgoCD Sync를 트리거하는 방식으로 서버의 ConfigMap, Deployment 등 파드 설정을 관리하고 있었습니다.
하지만 모든 서버가 동일한 환경이 아닌 데다, 배포 과정에서 네트워크나 방화벽 이슈로 인해 간헐적으로 휴먼 에러가 발생하는 상황이 있었습니다. ConfigMap의 키 하나가 다르거나, 특정 환경에서만 이미지 풀(pull)이 실패하는 상황 등이 그 예입니다. Git에는 변경이 정상적으로 반영되었더라도, 클러스터의 실제 상태는 다를 수 있다는 사실을 매번 마주하게 됩니다.
이로 인해 파드가 정상적으로 뜨지 않거나, Pod 이미지나 ConfigMap에 문제가 생겼을 때 빠르게 파악하지 못하는 경우가 종종 있었습니다. 결국 “Git에는 적용되었지만 운영에 반영되지 않은” 상태가 누적되며, 이를 해결하기 위해 운영자가 다시 손으로 확인하는 과정이 늘어나는 악순환이 시작되었습니다.
기대하는 동작
Kubernetes 클라이언트 라이브러리를 Spring Boot에 적용하면, 애플리케이션 자체가 클러스터를 능동적으로 관찰하고 이상 상황에 반응할 수 있게 됩니다. 제가 기대하는 동작은 명확합니다.
-
ConfigMap 변경이 의도한 방향과 다르게 적용되면, 해당 데이터를 관리하는 서버로 즉시 통지한다.
-
여러 특정 상황으로 인해 Pod가 정상적으로 작동하지 않았을 때, 트리거를 발생하여 알림을 준다.
결국 핵심은 사람이 직접 확인해야 했던 과정을 자동화하여, 휴먼 에러로 인한 장애를 사전에 막는 것입니다. 단순히 “편해진다”라는 차원이 아니라, 장애를 인지하기까지 걸리는 시간(MTTD)을 줄이는 운영 인프라적 의미가 큽니다.
어떻게 동작하는가 — 원리
사실 Kubernetes 클라이언트 라이브러리가 하는 일은 본질적으로 단순합니다. kube-apiserver의 REST API를 개발자가 편리하게 사용할 수 있도록 감싸놓은 래퍼입니다. Pod 목록을 조회하는 코드 한 줄을 작성하면, 라이브러리 내부에서는 API Server에 HTTP GET 요청을 보내고 응답을 자바 객체로 변환해 돌려주는 방식으로 동작합니다.
즉, 우리가 평소에 터미널에서 입력하는 kubectl get pods 와 동일한 흐름이, 라이브러리 호출 한 줄로 바뀌는 것뿐입니다. 차이라면 결과를 사람이 보는 텍스트가 아니라 PodList 같은 자바 객체로 받게 된다는 점, 그리고 그 객체를 그대로 비즈니스 로직에 넘길 수 있다는 점입니다.
API Server 접속 정보는 어디서 오는가
여기서 의문이 생깁니다. “API Server의 주소나 인증 정보를 개발자가 직접 설정해야 하나?” 답은 ‘아니오’입니다. 애플리케이션이 쿠버네티스 클러스터 위의 Pod로 배포되면, 즉 같은 클러스터 안에서 실행되면, 쿠버네티스가 필요한 모든 접속 정보를 자동으로 주입해줍니다.
구체적으로는 다음 세 가지가 자동으로 준비됩니다. API Server의 IP는 KUBERNETES_SERVICE_HOST 와 KUBERNETES_SERVICE_PORT 라는 환경변수로 주입됩니다. 클라이언트 라이브러리는 이 정보들을 알아서 읽어 연결을 구성하기 때문에, 개발자 입장에서는 사실상 아래 한 줄이면 설정이 끝납니다.
ApiClient client = new KubernetesClientBuilder().build();
이 한 줄이 가능한 이유는, 라이브러리가 “나는 지금 클러스터 안에 있는가?”를 자동으로 판단하고, 클러스터 안이라면 in-cluster 설정을, 밖이라면 ~/.kube/config 의 컨텍스트를 사용해 연결을 구성해주기 때문입니다. 로컬 개발 환경과 운영 환경에서 같은 코드가 동작할 수 있는 배경이 여기에 있습니다.
실시간 변화 감지 — Watch와 Informer
실시간 변화 감지도 지원합니다. 단순 조회 외에도 Watch라는 기능을 사용하면 HTTP 스트리밍 방식으로 클러스터의 이벤트를 실시간으로 수신할 수 있습니다. Pod가 추가되거나 삭제될 때, ConfigMap이 갱신될 때 즉각적으로 반응하는 로직을 구현할 수 있는 것입니다.
Watch는 본질적으로 long-running HTTP 연결입니다. 한 번 요청을 보내면 연결이 유지된 채 이벤트가 발생할 때마다 JSON 라인 단위로 데이터가 흘러들어옵니다. 그래서 Spring Boot에서 사용할 때는 별도의 스레드 또는 비동기 컨텍스트에서 처리하는 것이 일반적입니다.
운영 환경에서는 여기서 한 단계 더 나아간 Informer 패턴을 주로 사용합니다. Informer는 초기에 List API로 전체 상태를 가져온 뒤, 이어서 Watch로 변경 사항만 받아 로컬 캐시를 유지합니다. 즉 항상 “클러스터의 최신 상태”에 가까운 스냅샷을 메모리에 들고 있게 되며, 조회 요청은 API Server까지 가지 않고 캐시에서 곧바로 응답을 받게 됩니다.
이 구조의 장점은 두 가지입니다. 첫째, API Server에 걸리는 부하를 크게 줄일 수 있습니다. 둘째, 항상 최신 상태를 빠르게 조회할 수 있습니다. 비즈니스 로직 입장에서는 “Pod 목록을 줘”라고 묻기만 하면, Informer가 캐시에서 즉시 응답을 돌려줍니다.
정리하자면, 단발성 동기화 → REST 호출, 단순 변화 감지 → Watch, 운영급 상태 추적 → Informer 라는 세 가지 사용 패턴을 상황에 맞게 선택하면 됩니다.
클라이언트의 라이프사이클 관리
KubernetesClient 객체는 내부적으로 HTTP 커넥션 풀을 들고 있기 때문에, 매 요청마다 새로 만들어 쓰는 것은 비효율적입니다. 저는 @Configuration 클래스에서 싱글톤 빈으로 등록하고, 애플리케이션 종료 시점에 close()가 호출되도록 @PreDestroy 또는 DisposableBean을 함께 구현했습니다. 이 작은 차이가 누적되면 커넥션 누수와 좀비 스레드의 원인이 되기 때문에, 처음에 구조를 잡을 때 신경 써두는 편이 좋습니다.
보안적으로 주의해야 할 점
편의성이 높은 만큼, 잘못 사용했을 때의 파급도 크다는 점은 처음부터 인지하고 시작해야 합니다. 운영 환경에서 가장 많이 발생하는 실수와, 그것을 막기 위한 최소한의 가이드라인을 정리해보았습니다.
RBAC
가장 흔히 저지르는 실수는 애플리케이션에 과도한 권한을 부여하는 것입니다. RBAC(Role-Based Access Control) 설정을 구성할 때, 빠르게 개발하다 보면 모든 권한을 허용하거나 cluster-admin 역할을 그냥 바인딩하는 경우가 생깁니다. 개발 단계에서는 “일단 동작부터 시켜보자”라는 마음으로 넘어가기 쉽지만, 이 설정이 그대로 운영에 흘러들어가는 일은 생각보다 자주 발생합니다.
이렇게 하면 해당 Pod가 탈취되었을 때 공격자가 클러스터 전체에 접근할 수 있는 심각한 상황이 벌어질 수 있습니다. 컨테이너 이미지의 취약점, 의존성 라이브러리의 RCE, 혹은 단순한 SSRF 한 건이 곧바로 클러스터 전체의 침해로 이어질 수 있다는 뜻입니다.
실제로 필요한 리소스와 동작(get, list, watch)만 명시적으로 허용하고, 가능하면 ClusterRole보다 특정 네임스페이스에만 적용되는 Role을 우선 사용하는 것이 바람직합니다. 새 기능을 추가할 때마다 “이 기능에 정말로 추가 권한이 필요한가?”를 먼저 묻는 습관이 가장 강력한 방어선이 됩니다.
ServiceAccount 토큰 관리
ServiceAccount 토큰 관리도 주의해야 합니다. 기본적으로 쿠버네티스는 모든 Pod에 ServiceAccount 토큰을 자동으로 마운트합니다. 그런데 Kubernetes API에 접근할 필요가 없는 Pod에도 토큰이 마운트된다는 뜻이기도 합니다. 즉, 클러스터 안의 모든 Pod가 잠재적인 진입점이 될 수 있다는 의미입니다.
이를 방지하려면 API 접근이 필요 없는 Pod의 경우 automountServiceAccountToken: false 설정을 명시적으로 추가하여 불필요한 토큰 노출을 막아야 합니다. ServiceAccount 단위로도, Pod spec 단위로도 설정할 수 있으므로, 가능하면 둘 다 사용해 이중 방어를 구성하는 편을 권합니다.
감사 로그와 알림
마지막으로, 권한 자체를 통제하는 것 못지않게 “누가 무엇을 했는지”를 남기는 것도 중요합니다. 쿠버네티스는 audit log 기능을 기본 제공하므로, 우리 애플리케이션이 사용하는 ServiceAccount가 의도한 범위 밖의 호출을 시도하지 않는지 주기적으로 점검하는 절차를 운영 루틴에 포함시켜 두는 것이 좋습니다. 권한은 한 번 설정하고 끝나는 것이 아니라, 점검과 회고가 함께 따라붙어야 비로소 의미를 가집니다.
마무리
Spring Boot에서 Kubernetes 클라이언트를 사용하는 것은, 애플리케이션이 단순히 클러스터 위에서 실행되는 수준을 넘어 클러스터를 능동적으로 인식하고 활용하는 수준으로 올라서는 것을 의미합니다. 이전까지는 “플랫폼이 알아서 해주겠지”라며 외부에 맡겨두었던 운영의 일부를, 애플리케이션 코드 안으로 끌어와 함께 책임지는 형태가 됩니다.
자동화된 운영 도구나 플랫폼을 만들 때 강력한 선택지가 되지만, 그만큼 잘못 사용했을 때의 보안 리스크도 크기 때문에 권한 설정에 신중하게 접근하는 것이 좋습니다. 편의성이 보장된다면, 그만큼 기본 설정 값과 설정 체크를 꼼꼼히 하는 것이 중요하다고 생각합니다.
개인적으로 이번 도입을 통해 가장 크게 얻은 것은 “애플리케이션과 인프라의 경계가 점점 흐려지고 있다”는 감각이었습니다. 이전에는 인프라 팀의 영역이라 여겼던 클러스터 상태 관찰이, 이제는 우리 서비스 코드 안에서 자연스럽게 다뤄야 하는 일상의 일부가 되었습니다. 그리고 그 변화는, 운영자에게는 더 빠른 대응, 사용자에게는 더 짧은 장애 시간으로 이어집니다.
참고 자료
-
쿠버네티스 — 역할 기반 접근 제어(RBAC) 모범 사례 : https://kubernetes.io/ko/docs/concepts/security/rbac-good-practices/
-
Spring Boot — Kubernetes Client 사용 및 Service Account 설정 : https://docs.spring.io/spring-cloud-kubernetes/docs/current/reference/html/#service-account
Bang