zookeeper, kafka, kafka-ui에 Spring Boot 애플리케이션 하나. docker-compose로 잘 돌던 Kafka 예제를 로컬 k8s 클러스터에서도 돌려 보기로 했다. 환경은 Apple Silicon(arm64) Mac이다.
manifest 변환과 클러스터 구성은 도구가 거의 다 해 줬다. 막힌 곳은 한 군데였다. Registry에 올리지 않은 로컬 이미지를 minikube가 찾지 못했다. 이 글에서는 그 문제를 중심으로 클러스터를 띄우고 Pod끼리 통신과 외부 접속까지 확인한 과정을 정리한다.
compose 파일은 kompose로 변환했다
이미 있는 docker-compose.yml을 k8s용 yaml로 손으로 다시 쓸 이유는 없었다. kompose가 compose 파일을 읽어 Deployment와 Service로 바꿔 준다.
brew install kompose
kompose convert -f docker-compose.yml --out converted.yaml
이 converted.yaml을 minikube 클러스터에 그대로 적용한다.
minikube 클러스터를 띄운다
brew install minikube
minikube start

클러스터를 시작하면 ~/.kube/config에 클러스터와 컨텍스트 정보가 자동으로 설정된다. 출력을 보면 docker 드라이버로 떠서 그 안에 쿠버네티스 v1.28.3이 Docker 24.0.7 런타임으로 올라간다. 이 “안쪽의 Docker"가 뒤에서 문제가 된다.
선택 사항이지만, 앱 전용 Namespace를 만들어 현재 컨텍스트의 기본값으로 걸어 두면 이후 명령마다 -n을 붙이지 않아도 된다.
kubectl create ns kafka-app
kubectl config set-context --current --namespace=kafka-app
로컬에 있는 이미지를 minikube가 못 찾는 이유
yaml에 적힌 이미지는 먼저 로컬에서 찾고, 없으면 Registry에서 받아 온다. 그런데 Spring Boot 이미지는 분명 로컬에 있는데도 minikube는 Registry를 뒤지다 실패했고, Pod가 만들어지지 않았다.
여기서 “로컬"은 내 Mac이 아니라 minikube 노드다. minikube는 자기 안에 별도의 Docker 데몬을 두고 돌아간다. 호스트에서 docker build로 만든 이미지는 호스트 데몬에만 있고, 노드의 kubelet은 노드 쪽 데몬만 본다. 그러니 노드 입장에서 그 이미지는 없는 이미지다.
minikube docker-env는 지금 셸의 Docker 클라이언트가 minikube 안의 데몬을 바라보도록 환경 변수를 내준다. 이걸 eval로 적용한다.
eval $(minikube docker-env)
환경 변수라서 효과는 이 명령을 실행한 터미널에만 있다. 이미지 빌드는 반드시 같은 터미널에서 해야 이미지가 minikube 쪽 데몬에 생긴다.
리소스를 만들고 이미지를 빌드한다
변환해 둔 yaml로 k8s 리소스를 만든다.
kubectl apply -f converted.yaml
zookeeper, kafka, kafka-ui 관련 Deployment와 Service가 생성된다.
Spring Boot 애플리케이션은 Gradle로 빌드해 둔 jar를 Docker 이미지로 감싼다. arm64 Mac이라 베이스 이미지도 arm64v8/openjdk를 digest까지 고정해 썼다.
# 베이스 이미지를 지정
# 이미지가 로컬에 존재하지 않을 경우에만 다운로드
# 이미지가 로컬에 있을 경우에는 다시 다운로드 X
FROM arm64v8/openjdk:11-jdk-slim@sha256:482a519d80f99a32117003a369c14fdc180c3175ca3ee8b2bf659b9de05cf53b
# 작업 디렉토리를 설정
WORKDIR /app
# 호스트의 JAR 파일을 Docker 이미지로 복사
COPY build/libs/kafka-chat-0.0.1-SNAPSHOT.jar /app/kafka-chat-0.0.1-SNAPSHOT.jar
# 어플리케이션을 실행
CMD ["java", "-jar", "kafka-chat-0.0.1-SNAPSHOT.jar"]
eval을 실행한 그 터미널에서 Dockerfile이 있는 경로로 가서 빌드한다.
# [usage] docker build -t {생성할 image 명}:{지정할 tag 혹은 버전정보} .
docker build -t my-spring-application:0.0.14 .
태그를 latest가 아닌 버전으로 준 것도 의미가 있다. k8s는 latest 태그나 태그 없는 이미지에는 기본 imagePullPolicy를 Always로 잡아서, 노드에 이미지가 있어도 Registry에서 다시 받으려 한다. 버전 태그면 기본값이 IfNotPresent라 노드에 있는 이미지를 그대로 쓴다.
대시보드와 로그로 확인한다
minikube dashboard
프록시가 뜨고 출력된 URL로 접속하면 생성된 k8s 리소스를 볼 수 있다.


화면 상단에서 Namespace가 앞에서 만든 kafka-app으로 잡혀 있는지 확인한다.
로그는 kubectl logs로 본다.
kubectl logs pod/kafka-ui-57f44b6bb8-cgwzq
Pod끼리 통신되는지 nc로 두드려 본다
kafka-ui Pod에서 kafka Pod의 9092 포트로 연결이 되는지 확인했다. 이미지에 netcat이 없어서 Pod에 들어가 먼저 설치했다.
# Pod(ex: kafka-ui) 접속
kubectl exec -it pod/kafka-ui-57f44b6bb8-cgwzq -- sh
# netcat 설치
apk update && apk add netcat-openbsd
# 다른 Pod(kafka server)로의 통신 체크
# [usage] nc -zv {수신 target container IP} {open 되어있는지 확인하고자 하는 port}
nc -zv 10.244.0.74 9092
-z: 데이터를 보내지 않고 연결만 시도한다.-v: 자세한 출력(Verbose)을 켜서 연결 과정을 보여 준다.
결과는 이렇게 갈린다. 실패 예시는 열려 있지 않은 9094 포트로 시도했을 때다.
# 성공
Connection to 10.244.0.74 9092 port [tcp/*] succeeded!
# 실패 (9094 포트로 시도)
connect to 10.244.0.74 port 9094 (tcp) failed: Connection refused
배포판별 netcat 설치 명령은 포트가 열렸는지 볼 때 쓰는 nc와 ss에 따로 모았다.
kafka-ui를 로컬 브라우저에서 연다
클러스터 안의 Service는 그대로는 Mac 브라우저에서 닿지 않는다. minikube service가 로컬과 Service 사이에 터널을 열어 주는데, 이때 새 포트가 생기고 그 포트로 터널링된다.
# minikube 클러스터 내 특정 서비스 <--> local 터널링 (새로운 port 생성되며 해당 포트에서 터널링)
minikube service kafka-ui --url --namespace {애플리케이션 구성되어있는 네임스페이스이름}
# OR # minikube 클러스터 내 전체 서비스 <--> local 터널링 (새로운 port 생성되며 해당 포트에서 터널링)
minikube service --all --namespace {애플리케이션 구성되어있는 네임스페이스이름}
# (option) 위 과정으로 생성된 포트 확인(위 명령으로 확인된 포트와 일치하는지 확인), 다른 터미널에서 실행
ps -ef | grep [email protected]
# 브라우저에서 접속

ps로 보면 minikube service가 띄운 ssh 프로세스가 있다. -L 64706:10.96.225.96:29092, 즉 로컬 64706 포트를 클러스터 안 Service의 IP와 포트로 넘기는 SSH 포트포워딩이다. 브라우저에서는 이 로컬 포트로 접속한다.
Spring Boot Pod가 8080을 듣고 메시지를 받는지 본다
마지막으로 Spring Boot 애플리케이션 Pod에 들어가 8080 포트를 리스닝하는지 확인하고, 메시지를 한 건 보내 로그를 봤다.
# 스프링 부트 애플리케이션 pod로 접속
kubectl exec -it pod/spring-boot-application-647647954d-g9rxp -- sh
# curl 설치
apt-get update && apt-get install -y curl
# 리스닝하고있는 포트 확인하기 위해 net-tools 설치
apt-get install -y net-tools
# 리스닝하고있는 포트 확인
netstat -tuln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN
8080 포트를 리스닝하고 있다. 다른 터미널에서 로그를 걸어 두고 메시지를 보낸다.
# 다른터미널에서 스프링 부트 애플리케이션 pod 로그확인
kubectl logs -f spring-boot-application-647647954d-g9rxp
curl --location 'http://localhost:8080/kafka/send/message' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'message=kafka chat sample'
compose 파일 변환과 클러스터 구성은 kompose와 minikube 명령 몇 줄로 끝났다. 막힌 곳은 이미지가 어느 Docker 데몬에 있느냐 하나였다. 이미지를 빌드하기 전에 eval $(minikube docker-env)부터 치는 습관을 들이면 같은 자리에서 다시 막히지 않는다.