포스트

K8s 환경에서 Kafka, Redis, Kafka UI 구축 — Spring Boot 멀티 포트 연동 트러블슈팅

로컬 K8s(Docker Desktop)에 Kafka와 Redis를 배포하고, 호스트의 Spring Boot와 클러스터 내부의 Kafka UI가 동시에 카프카에 접속하도록 Internal/External 이중 리스너를 구성한 과정을 정리합니다.

K8s 환경에서 Kafka, Redis, Kafka UI 구축 — Spring Boot 멀티 포트 연동 트러블슈팅

문제 상황

K8s 안에 떠 있는 Kafka를 호스트(Mac)의 Spring Boot와, 같은 클러스터 안 Pod로 떠 있는 Kafka UI가 동시에 접속하려고 하면 한쪽이 연결에 실패합니다.

원인은 KAFKA_ADVERTISED_LISTENERS에 있습니다. 클라이언트는 브로커에 처음 접속할 때 메타데이터 응답으로 “이 주소로 접속해라”라는 advertised host를 받는데, 리스너를 하나만 두면 호스트 OS와 클러스터 내부 Pod 중 한쪽은 그 주소로 브로커를 찾을 수 없어서 연결 재시도만 반복하다 실패합니다.

해결책은 Kafka의 멀티 리스너 설정으로 내부용 포트(9092)와 외부용 포트(9094)를 완전히 분리하는 것입니다.

인프라 구성

Kafka 배포 (이중 포트)

Internal(9092)과 External(9094) 리스너를 분리해서 배포합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apiVersion: v1
kind: Service
metadata:
  name: my-kafka
spec:
  ports:
  - port: 9092
    targetPort: 9092
    name: internal
  - port: 9094
    targetPort: 9094
    name: external
  selector:
    app: kafka
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-kafka
spec:
  serviceName: my-kafka
  replicas: 1
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      containers:
      - name: kafka
        image: apache/kafka:3.7.0
        ports:
        - containerPort: 9092
        - containerPort: 9093
        - containerPort: 9094
        env:
        - name: KAFKA_NODE_ID
          value: "1"
        - name: KAFKA_PROCESS_ROLES
          value: "controller,broker"
        - name: KAFKA_LISTENERS
          value: "INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094"
        - name: KAFKA_ADVERTISED_LISTENERS
          value: "INTERNAL://my-kafka:9092,EXTERNAL://127.0.0.1:9094"
        - name: KAFKA_CONTROLLER_LISTENER_NAMES
          value: "CONTROLLER"
        - name: KAFKA_CONTROLLER_QUORUM_VOTERS
          value: "1@localhost:9093"
        - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
          value: "CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT"
        - name: KAFKA_INTER_BROKER_LISTENER_NAME
          value: "INTERNAL"
        - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR
          value: "1"

핵심은 KAFKA_ADVERTISED_LISTENERS가 리스너별로 다른 주소를 광고한다는 점입니다. INTERNAL은 클러스터 내부 DNS(my-kafka:9092)를, EXTERNAL은 포트포워딩 이후 호스트가 접근할 주소(127.0.0.1:9094)를 알려줍니다.

Kafka UI 배포

Kafka UI는 K8s 내부 Pod이므로 Kafka의 Internal 포트를 바라보게 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kafka-ui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kafka-ui
  template:
    metadata:
      labels:
        app: kafka-ui
    spec:
      containers:
      - name: kafka-ui
        image: provectuslabs/kafka-ui:latest
        ports:
        - containerPort: 8080
        env:
        - name: KAFKA_CLUSTERS_0_NAME
          value: "K8s-Kafka"
        - name: KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS
          value: "my-kafka:9092"
---
apiVersion: v1
kind: Service
metadata:
  name: kafka-ui
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 8080
    nodePort: 30080
  selector:
    app: kafka-ui

http://localhost:30080으로 접속하면 Kafka UI가 Internal 리스너를 통해 브로커에 정상적으로 붙습니다.

Redis 배포 (NodePort)

1
2
3
4
5
helm upgrade --install my-redis bitnami/redis \
  --set architecture=standalone \
  --set auth.enabled=false \
  --set master.service.type=NodePort \
  --set master.service.nodePorts.redis=30379

localhost:30379로 접속합니다.

Spring Boot 연동

호스트(Mac)의 Spring Boot는 External 포트(9094)를 포트포워딩해서 씁니다.

1
kubectl port-forward svc/my-kafka 9094:9094
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
  data:
    redis:
      host: localhost
      port: 30379

  kafka:
    bootstrap-servers: 127.0.0.1:9094
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      group-id: delivery-external-api-group
      enable-auto-commit: false
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      properties:
        spring.json.trusted.packages: "*"
    listener:
      ack-mode: MANUAL

정리

  • 문제의 본질은 “advertised address는 리스너마다 달라야 한다”는 것 하나입니다.
  • 내부 접속자(같은 클러스터 안 Pod)와 외부 접속자(호스트 OS)를 같은 포트/같은 주소로 섞으려고 하면 반드시 한쪽이 깨집니다.
  • kubectl port-forward는 세션이 떠 있는 동안만 유효하므로, 로컬 개발 중에는 별도 터미널에 계속 띄워둬야 합니다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.