
Intro
안녕하세요. 환이s입니다 👋
지난 글에서는 데이터 레이크하우스 플랫폼의
두 번째 스택으로 Spark Operator를 Kubernetes 위에 구축하고
Delta Lake + Ceph S3 연동까지 정리해 봤습니다.
이번 글에서는 세 번째 스택,
Apache Kafka를 Kubernetes 환경에 구축하는 과정을 정리해보려고 합니다.

Kafka를 Kubernetes 위에서 운영하다 보면
단순히 브로커를 띄우는 것에서 끝나는 게 아니라,
- Strimzi Operator가 Kafka 클러스터를 Kubernetes 리소스로 선언형으로 관리하고
- KRaft 모드로 Zookeeper 없이 브로커가 직접 메타데이터를 관리하고
- KafkaNodePool로 브로커를 풀 단위로 나눠서 운영하고
- AKHQ로 토픽과 메시지를 UI로 모니터링하고
- Producer API가 Kafka에 메시지를 발행하는
전체 흐름이 유기적으로 연결되어야 합니다.
이번 글에서는
- Strimzi Operator 아키텍처가 어떻게 동작하는지
- KRaft 모드가 무엇이고 왜 사용하는지
- 실제로 어떤 순서로 설치하면 되는지
- KafkaNodePool, KafkaTopic, AKHQ까지 어떻게 구성하는지
까지 순서대로 확인해 보겠습니다.
1. Strimzi Operator 아키텍처 개요
본격적인 설치에 앞서,
Strimzi Operator가 어떤 구조로 동작하는지 먼저 이해하는 것이 중요합니다.
Strimzi는 Kubernetes 위에서 Kafka 클러스터를 Custom Resource로 선언해서 운영할 수 있게 해주는 오퍼레이터입니다.
동작 흐름은 아래와 같습니다.
사용자가 Kafka / KafkaNodePool yaml을 apply
↓
Strimzi Operator가 리소스 감지
↓
Kafka 브로커 Pod 생성 (KRaft 모드)
↓
Entity Operator → KafkaTopic / KafkaUser 관리
↓
AKHQ → Kafka 클러스터 UI 모니터링
↓
Producer → 토픽에 메시지 발행
↓
Spark Streaming → 토픽에서 메시지 소비 → Delta Lake 저장
이번 구성에서 사용하는 주요 CRD는 아래와 같습니다.
| CRD | 역할 |
| Kafka | Kafka 클러스터 전체 설정 |
| KafkaNodePool | 브로커 풀 단위 설정 |
| KafkaTopic | 토픽 생성 및 설정 |
| KafkaUser | 사용자 인증 관리 |
| KafkaConnect | Kafka Connect 설정 |
KRaft 모드란?
기존 Kafka는 메타데이터 관리를 위해 Zookeeper가 별도로 필요했습니다.
KRaft(Kafka Raft) 모드는 Kafka 자체적으로 메타데이터를 관리하는 방식으로,
Zookeeper 없이 브로커만으로 클러스터를 운영할 수 있습니다.
| 구분 | Zookeeper 모드 | KRaft 모드 |
| 메타데이터 관리 | Zookeeper 별도 운영 | Kafka 자체 관리 |
| 운영 복잡도 | 높음 | 낮음 |
| 장애 포인트 | Kafka + Zookeeper | Kafka만 |
| Kafka 버전 | 모든 버전 | 3.3 이상 권장 |
즉, 여기서 확인할 수 있는 핵심은
KRaft 모드를 사용하면 Zookeeper 운영 부담 없이 Kafka 클러스터만으로 심플하게 운영할 수 있다는 점입니다.
2. 설치 순서
이번 구축에서 사용하는 디렉토리 구조는 아래와 같습니다.
kafka/
├── kustomize/ # Strimzi Operator 설치 (kustomize)
│ ├── install/cluster-operator/
│ └── kustomization.yaml
│
└── manifests/
├── 00_kafka-local-path/ # LocalPath StorageClass (Kafka 전용)
├── 01_rbac/ # ServiceAccount + Secret
├── 10_nodepool/ # KafkaNodePool (pool-a, pool-b)
├── 20_cluster/ # Kafka 클러스터 설정
├── 30_topic/ # KafkaTopic 생성
├── 40_akhq/ # AKHQ (Kafka UI)
└── 50_producer/ # Producer API
설치 순서는 아래와 같습니다.
kustomize(Operator) → 00_local-path → 01_rbac → 10_nodepool
→ 20_cluster → ⏳ → 30_topic → 40_akhq → 50_producer
Spark 편과 마찬가지로
Kafka 클러스터가 완전히 Ready 상태가 된 이후에
KafkaTopic을 apply해야 Entity Operator가 정상적으로 토픽을 생성합니다.
3. 사전 준비 : 노드 레이블 설정
Kafka 브로커는 role=kafka 레이블이 있는 노드에만 배포됩니다.
이번 구성에서는 worker1, worker2 두 노드를 Kafka 전용으로 사용합니다.
kubectl label node k8s-worker1 role=kafka
kubectl label node k8s-worker2 role=kafka
레이블 확인은 아래 명령어로 할 수 있습니다.
kubectl get nodes --show-labels | grep kafka
아래처럼 두 노드에 레이블이 붙어있으면 정상입니다.
k8s-worker1 Ready ... role=kafka
k8s-worker2 Ready ... role=kafka
즉, 여기서 확인할 수 있는 핵심은
노드 레이블이 사전에 설정되어 있어야 KafkaNodePool의 nodeAffinity 설정이 정상적으로 동작한다는 점입니다.
4. Strimzi Operator 설치(kustomize)
Strimzi Operator는 공식 파일을 기반으로 kustomize를 활용해서 설치합니다.
핵심은 kustomization.yaml에서 두 가지를 커스텀했다는 점입니다.
# kustomize/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kafka
resources:
- install/cluster-operator/010-ServiceAccount-strimzi-cluster-operator.yaml
- install/cluster-operator/020-ClusterRole-strimzi-cluster-operator-role.yaml
- install/cluster-operator/020-RoleBinding-strimzi-cluster-operator.yaml
- install/cluster-operator/021-ClusterRoleBinding-strimzi-cluster-operator.yaml
- install/cluster-operator/021-ClusterRole-strimzi-cluster-operator-role.yaml
- install/cluster-operator/022-ClusterRole-strimzi-cluster-operator-role.yaml
- install/cluster-operator/022-RoleBinding-strimzi-cluster-operator.yaml
- install/cluster-operator/023-ClusterRole-strimzi-cluster-operator-role.yaml
- install/cluster-operator/023-RoleBinding-strimzi-cluster-operator.yaml
- install/cluster-operator/030-ClusterRoleBinding-strimzi-cluster-operator-kafka-broker-delegation.yaml
- install/cluster-operator/030-ClusterRole-strimzi-kafka-broker.yaml
- install/cluster-operator/031-ClusterRole-strimzi-entity-operator.yaml
- install/cluster-operator/031-RoleBinding-strimzi-cluster-operator-entity-operator-delegation.yaml
- install/cluster-operator/033-ClusterRoleBinding-strimzi-cluster-operator-kafka-client-delegation.yaml
- install/cluster-operator/033-ClusterRole-strimzi-kafka-client.yaml
- install/cluster-operator/040-Crd-kafka.yaml
- install/cluster-operator/041-Crd-kafkaconnect.yaml
- install/cluster-operator/042-Crd-strimzipodset.yaml
- install/cluster-operator/043-Crd-kafkatopic.yaml
- install/cluster-operator/044-Crd-kafkauser.yaml
- install/cluster-operator/045-Crd-kafkamirrormaker.yaml
- install/cluster-operator/046-Crd-kafkabridge.yaml
- install/cluster-operator/047-Crd-kafkaconnector.yaml
- install/cluster-operator/048-Crd-kafkamirrormaker2.yaml
- install/cluster-operator/049-Crd-kafkarebalance.yaml
- install/cluster-operator/04A-Crd-kafkanodepool.yaml
- install/cluster-operator/050-ConfigMap-strimzi-cluster-operator.yaml
- install/cluster-operator/060-Deployment-strimzi-cluster-operator.yaml
patches:
- target:
kind: Deployment
name: strimzi-cluster-operator
patch: |-
- op: add
path: /spec/template/spec/affinity
value:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values:
- kafka
replacements:
- source:
kind: ServiceAccount
name: strimzi-cluster-operator
fieldPath: metadata.namespace
targets:
- select:
kind: RoleBinding
fieldPaths:
- subjects.[name=strimzi-cluster-operator].namespace
- select:
kind: ClusterRoleBinding
fieldPaths:
- subjects.[name=strimzi-cluster-operator].namespace
install/cluster-operator/ 안의 파일들은
아래 Strimzi 공식 GitHub에서 그대로 가져와서 사용합니다.
단, kustomization.yaml에서 nodeAffinity 패치와
namespace 치환 설정을 추가로 커스텀했습니다.
strimzi-kafka-operator/install/cluster-operator at main · strimzi/strimzi-kafka-operator
Apache Kafka® running on Kubernetes. Contribute to strimzi/strimzi-kafka-operator development by creating an account on GitHub.
github.com
nodeAffinity 패치
patches:
- target:
kind: Deployment
name: strimzi-cluster-operator
patch: |-
- op: add
path: /spec/template/spec/affinity
value:
nodeAffinity:
...
values:
- kafka
Operator Pod 자체도 role=kafka 노드에 고정되도록 패치를 적용했습니다.
RBAC namespace 치환
replacements:
- source:
kind: ServiceAccount
name: strimzi-cluster-operator
fieldPath: metadata.namespace
targets:
- select:
kind: RoleBinding
fieldPaths:
- subjects.[name=strimzi-cluster-operator].namespace
공식 파일의 RoleBinding / ClusterRoleBinding에 있는
namespace를 kustomize가 자동으로 kafka로 치환해 줍니다.
이 설정이 없으면 RBAC 주체의 namespace가
공식 기본값인 myproject로 남아서 권한 오류가 발생할 수 있습니다.
kubectl apply -k kustomize/
Operator Pod가 정상적으로 올라왔는지 확인합니다.
kubectl get pods -n kafka | grep strimzi
아래처럼 Running 상태가 되면 다음 단계로 진행합니다.
NAME READY STATUS RESTARTS AGE
strimzi-cluster-operator-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
즉, 여기서 확인할 수 있는 핵심은
kustomize를 활용하면 공식 파일을 직접 수정하지 않고도 nodeAffinity, namespace 같은 환경별 설정을 패치로 관리할 수 있다는 점입니다.
5. LocalPath StorageClass 설치
Kafka 브로커의 데이터 저장을 위해 Kafka 전용 LocalPath StorageClass를 별도로 구성했습니다.
# 00_kafka-local-path/01_local-path.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: kafka-local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
---
apiVersion: v1
kind: ConfigMap
metadata:
name: local-path-config
namespace: kafka
data:
config.json: |-
{
"nodePathMap":[
{
"node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths":["/data/kafka-storage"]
}
]
}
주요 설정 포인트는 아래와 같습니다.
kafka-local-path StorageClass를 별도로 만든 이유
Ceph StorageClass(rook-ceph-block)를 사용하지 않고
노드 로컬 디스크를 직접 사용하는 이유는 Kafka의 특성 때문입니다.
Kafka는 대용량 메시지를 순차적으로 빠르게 쓰는 워크로드라서
네트워크를 경유하는 Ceph보다 로컬 디스크가 레이턴시 면에서 유리합니다.
reclaimPolicy: Retain
reclaimPolicy: Retain
PVC가 삭제되어도 실제 데이터는 노드에 보존됩니다.
Kafka 데이터는 메시지 유실이 치명적이기 때문에 Retain으로 설정했습니다.
저장 경로
"paths":["/data/kafka-storage"]
모든 노드에서 /data/kafka-storage 경로를 Kafka 데이터 저장소로 사용합니다.
적용 전에 해당 경로가 존재하는지 확인해 두는 것이 좋습니다.
# 각 kafka 노드에서 경로 생성
mkdir -p /data/kafka-storage
kubectl apply -f manifests/00_kafka-local-path/
즉, 여기서 확인할 수 있는 핵심은
Kafka는 로컬 디스크 기반 스토리지를 사용해서 네트워크 스토리지 대비 쓰기 레이턴시를 낮춘다는 점입니다.
6. RBAC 설정
# 01_rbac/01_kafka-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: kafka-sa
namespace: kafka
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kafka-api-role
namespace: kafka
rules:
- apiGroups: ["kafka.strimzi.io"]
resources:
- kafkas
- kafkatopics
- kafkausers
- kafkaconnects
- kafkaconnectors
- kafkabridges
- kafkamirrormaker2s
- kafkarebalances
- kafkanodepools
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["kafka.strimzi.io"]
resources:
- kafkas/status
- kafkatopics/status
- kafkausers/status
- kafkaconnects/status
- kafkaconnectors/status
- kafkabridges/status
- kafkamirrormaker2s/status
- kafkarebalances/status
- kafkanodepools/status
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources:
- pods
- pods/log
- services
- configmaps
- secrets
- events
- persistentvolumeclaims
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kafka-api-rb
namespace: kafka
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kafka-api-role
subjects:
- kind: ServiceAccount
name: kafka-sa
namespace: kafka
---
# 01_rbac/02_kafka-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: kafka-sa-token
namespace: kafka
annotations:
kubernetes.io/service-account.name: kafka-sa
type: kubernetes.io/service-account-token
Spark 편의 spark-sa와 달리
Kafka의 kafka-sa는 Role(Namespace 범위)로 구성했습니다.
KafkaTopic, KafkaUser 등 Strimzi CRD 리소스가 모두 kafka Namespace 안에서만 동작하기 때문입니다.
kubectl apply -f manifests/01_rbac/
7. KafkaNodePool 설정
KafkaNodePool은 Kafka 브로커를 풀 단위로 나눠서 관리하는 리소스입니다.
이번 구성에서는 pool-a, pool-b 두 개의 풀로 총 4개의 브로커를 구성했습니다.
# 10_nodepool/10_kafka-nodepool.yaml
# pool-a
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
name: pool-a
namespace: kafka
labels:
strimzi.io/cluster: kafka-cluster-a
spec:
replicas: 2
roles:
- controller
- broker
resources:
requests:
memory: 1Gi
cpu: "300m"
limits:
memory: 2Gi
cpu: "500m"
jvmOptions:
"-Xms": "512M"
"-Xmx": "512M"
storage:
type: persistent-claim
class: kafka-local-path
size: 50Gi
deleteClaim: true
template:
pod:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values:
- kafka
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: strimzi.io/name
operator: In
values:
- kafka-cluster-a-kafka
topologyKey: "kubernetes.io/hostname"
---
# pool-b (pool-a와 동일 구성, 이름만 다름)
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaNodePool
metadata:
name: pool-b
namespace: kafka
labels:
strimzi.io/cluster: kafka-cluster-a
spec:
replicas: 2
roles:
- controller
- broker
# ... (pool-a와 동일)
주요 설정 포인트는 아래와 같습니다.
roles: controller + broker
roles:
- controller
- broker
KRaft 모드에서는 브로커가 controller 역할도 겸합니다.
별도 Zookeeper나 controller 전용 노드 없이 브로커 자체가 메타데이터를 관리합니다.
pool-a / pool-b 분리 이유
pool-a → worker1, worker2에 2개 배포
pool-b → worker1, worker2에 2개 배포
총 4개 브로커 운영
풀을 분리하면 특정 풀만 롤링 업데이트하거나 풀별로 리소스를 다르게 설정하는 등 유연한 운영이 가능합니다.
podAntiAffinity (preferred)
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
required 대신 preferred를 사용한 이유는
노드가 2대(worker1, worker2)인데 브로커가 4개이기 때문입니다.
required로 설정하면 노드당 1개 제한이 걸려서 4개가 전부 뜨지 못합니다.
preferred로 설정하면 가능하면 다른 노드에 배치하되, 불가피하면 같은 노드에도 배치할 수 있게 됩니다.
kubectl apply -f manifests/10_nodepool/
8. Kafka 클러스터 설정
# 20_cluster/20_kafka-cluster-a.yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: kafka-cluster-a
namespace: kafka
annotations:
strimzi.io/node-pools: "enabled"
strimzi.io/kraft: "enabled"
spec:
kafka:
version: 3.7.0
listeners:
- name: plain
port: 9092
type: internal
tls: false
- name: external
port: 9094
type: nodeport
tls: false
configuration:
bootstrap:
nodePort: 30130
config:
default.replication.factor: 2
min.insync.replicas: 1
offsets.topic.replication.factor: 2
transaction.state.log.replication.factor: 2
transaction.state.log.min.isr: 1
unclean.leader.election.enable: "false"
auto.create.topics.enable: "false"
log.retention.hours: 168
entityOperator:
topicOperator: {}
userOperator: {}
주요 설정 포인트는 아래와 같습니다.
KRaft 모드 활성화
annotations:
strimzi.io/node-pools: "enabled"
strimzi.io/kraft: "enabled"
이 두 어노테이션이 있어야 KafkaNodePool과 KRaft 모드가 활성화됩니다.
리스너 구성
listeners:
- name: plain
port: 9092
type: internal # 클러스터 내부 통신용
- name: external
port: 9094
type: nodeport # 외부 접근용 (NodePort 30130)
configuration:
bootstrap:
nodePort: 30130
내부 통신은 9092(plain), 외부 접근은 9094(NodePort 30130)로 분리했습니다.
Spark Streaming Job은 클러스터 내부에서 9092로 접근합니다.
주요 Kafka 설정
config:
default.replication.factor: 2 # 기본 replica 2개
min.insync.replicas: 1 # 최소 1개 ISR 보장
unclean.leader.election.enable: "false" # 데이터 유실 방지
auto.create.topics.enable: "false" # 토픽 자동 생성 비활성화
log.retention.hours: 168 # 메시지 보존 7일
auto.create.topics.enable: false로 설정해서
존재하지 않는 토픽으로 메시지를 발행할 때 자동으로 토픽이 생성되는 것을 방지합니다.
토픽은 반드시 KafkaTopic 리소스로 명시적으로 생성해야 합니다.
Entity Operator
entityOperator:
topicOperator: {}
userOperator: {}
Entity Operator는
KafkaTopic, KafkaUser 리소스를 감시해서 실제 Kafka 토픽과 사용자를 자동으로 생성/관리합니다.
kubectl apply -f manifests/20_cluster/
Kafka 클러스터가 완전히 올라올 때까지 기다립니다.
kubectl get kafka -n kafka -w
아래처럼 READY 상태가 되면 다음 단계로 진행합니다.
NAME DESIRED KAFKA REPLICAS READY ...
kafka-cluster-a 4 True ...
브로커 Pod 4개도 확인합니다.
kubectl get pods -n kafka | grep kafka-cluster
kafka-cluster-a-pool-a-0 1/1 Running 0 ...
kafka-cluster-a-pool-a-1 1/1 Running 0 ...
kafka-cluster-a-pool-b-0 1/1 Running 0 ...
kafka-cluster-a-pool-b-1 1/1 Running 0 ...
즉, 여기서 확인할 수 있는 핵심은
Kafka CR의 READY 상태가 True가 된 이후에만 Entity Operator가 KafkaTopic을 정상적으로 생성할 수 있다는 점입니다.
9. KafkaTopic 생성
Kafka 클러스터가 Ready 상태가 되면 토픽을 생성합니다.
이번 구성에서는 syslog와 traffic 두 가지 토픽을 생성합니다.
# 30_topic/30_topic.yaml
# syslog 토픽
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
name: syslog-topic
namespace: kafka
labels:
strimzi.io/cluster: kafka-cluster-a
spec:
partitions: 3
replicas: 2
config:
retention.ms: 86400000 # 메시지 보존 1일 (ms)
retention.bytes: 16106127360 # 파티션당 최대 15GB
segment.ms: 3600000 # 세그먼트 롤링 1시간
max.message.bytes: 10485760 # 최대 메시지 크기 10MB
---
# traffic 토픽
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
name: traffic-topic
namespace: kafka
labels:
strimzi.io/cluster: kafka-cluster-a
spec:
partitions: 3
replicas: 2
config:
retention.ms: 86400000
retention.bytes: 16106127360
segment.ms: 3600000
max.message.bytes: 10485760
주요 설정 포인트는 아래와 같습니다.
partitions: 3 / replicas: 2
partitions: 3 # Spark Executor 3개와 1:1 매핑
replicas: 2 # 브로커 장애 시 데이터 보존
파티션 수를 Spark Executor 수(3개)와 맞춰두면
각 Executor가 파티션 하나씩 병렬로 처리할 수 있어서 처리 효율이 좋아집니다.
retention 설정
retention.ms: 86400000 # 1일 보존
retention.bytes: 16106127360 # 파티션당 최대 15GB
두 조건 중 하나라도 초과하면 오래된 메시지부터 삭제됩니다.
kubectl apply -f manifests/30_topic/
토픽이 정상적으로 생성되었는지 확인합니다.
kubectl get kafkatopic -n kafka
NAME CLUSTER PARTITIONS REPLICATION FACTOR READY
syslog-topic kafka-cluster-a 3 2 True
traffic-topic kafka-cluster-a 3 2 True
즉, 여기서 확인할 수 있는 핵심은
strimzi.io/cluster 레이블이 Kafka CR의 이름과 일치해야 Entity Operator가 올바른 클러스터에 토픽을 생성한다는 점입니다.
10. AKHQ: Kafka UI 구성
AKHQ는 Kafka 클러스터의 토픽, 메시지, 컨슈머 그룹을 웹 UI로 모니터링할 수 있는 도구입니다.
# 40_akhq/41_akhq-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: akhq
namespace: kafka
spec:
replicas: 1
selector:
matchLabels:
app: akhq
template:
metadata:
labels:
app: akhq
spec:
containers:
- name: akhq
image: harbor.local/library/akhq:0.23.0
env:
- name: AKHQ_CONFIGURATION
value: |
akhq:
connections:
kafka:
properties:
bootstrap.servers: "kafka-cluster-a-kafka-bootstrap.kafka.svc.cluster.local:9092"
---
# 40_akhq/42_akhq-service.yaml
apiVersion: v1
kind: Service
metadata:
name: akhq
namespace: kafka
spec:
selector:
app: akhq
ports:
- port: 8080
targetPort: 8080
nodePort: 30131
type: NodePort
---
# 40_akhq/43_akhq-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kafka-ui
namespace: kafka
annotations:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "Authorization, Content-Type"
spec:
ingressClassName: nginx
rules:
- host: kafka.192.168.2.240.sslip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: akhq
port:
number: 8080
주요 설정 포인트는 아래와 같습니다.
bootstrap.servers 내부 주소
bootstrap.servers: "kafka-cluster-a-kafka-bootstrap.kafka.svc.cluster.local:9092"
Strimzi가 자동으로 생성하는 bootstrap 서비스 주소입니다.
형식은 <클러스터명>-kafka-bootstrap.<네임스페이스>.svc.cluster.local입니다.
AKHQ 접근은 아래 두 가지 방법으로 할 수 있습니다.
# NodePort로 직접 접근
http://<노드IP>:30131
# Ingress 도메인으로 접근
<http://kafka.192.168.2.240.sslip.io>
kubectl apply -f manifests/40_akhq/
즉, 여기서 확인할 수 있는 핵심은
AKHQ를 통해 토픽 목록, 파티션 상태, 메시지 내용, 컨슈머 그룹 lag을 한눈에 모니터링할 수 있다는 점입니다.
11. Producer API 배포
Producer는 외부 시스템에서 Kafka 토픽으로 메시지를 발행하는 커스텀 API 서버입니다.
# 50_producer/51_producer_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kafka-producer
namespace: kafka
spec:
replicas: 1
selector:
matchLabels:
app: kafka-producer
template:
metadata:
labels:
app: kafka-producer
spec:
containers:
- name: kafka-producer
image: harbor.local/library/kafka-producer-api:1.1
imagePullPolicy: Always
ports:
- containerPort: 8000
resources:
requests:
cpu: "500m"
memory: "500Mi"
limits:
cpu: "1"
memory: "1Gi"
---
# 50_producer/52_producer_service.yaml
apiVersion: v1
kind: Service
metadata:
name: kafka-producer
namespace: kafka
spec:
type: NodePort
selector:
app: kafka-producer
ports:
- name: http
port: 8000
targetPort: 8000
nodePort: 30136
Producer API는 NodePort:30136으로 외부에서 접근할 수 있습니다.
kubectl apply -f manifests/50_producer/
Producer가 정상적으로 올라왔는지 확인합니다.
kubectl get pods -n kafka | grep producer
NAME READY STATUS RESTARTS AGE
kafka-producer-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
12. 전체 구성 검증
모든 설치가 완료된 후 최종 상태를 확인합니다.
# 전체 Pod 상태
kubectl get pods -n kafka
# Kafka 클러스터 상태
kubectl get kafka -n kafka
# KafkaTopic 상태
kubectl get kafkatopic -n kafka
# KafkaNodePool 상태
kubectl get kafkanodepool -n kafka
정상적인 경우 아래처럼 보입니다.
# Pod 상태
NAME READY STATUS AGE
strimzi-cluster-operator-xxxxxxxxxx-xxxxx 1/1 Running ...
kafka-cluster-a-pool-a-0 1/1 Running ...
kafka-cluster-a-pool-a-1 1/1 Running ...
kafka-cluster-a-pool-b-0 1/1 Running ...
kafka-cluster-a-pool-b-1 1/1 Running ...
kafka-cluster-a-entity-operator-xxxxxxxxx 2/2 Running ...
akhq-xxxxxxxxxx-xxxxx 1/1 Running ...
kafka-producer-xxxxxxxxxx-xxxxx 1/1 Running ...
# Kafka 클러스터 상태
NAME DESIRED KAFKA REPLICAS READY
kafka-cluster-a 4 True
# KafkaTopic 상태
NAME CLUSTER PARTITIONS REPLICATION FACTOR READY
syslog-topic kafka-cluster-a 3 2 True
traffic-topic kafka-cluster-a 3 2 True
각 UI / API 접근 주소 정리입니다.
| 서비스 | 주소 | 용도 |
| AKHQ (NodePort) | http://<노드IP>:30131 | Kafka UI 모니터링 |
| AKHQ (Ingress) | http://kafka.192.168.2.240.sslip.io | Kafka UI 모니터링 |
| Kafka External | <노드IP>:30130 | 외부 클라이언트 접근 |
| Producer API | http://<노드IP>:30136 | 메시지 발행 API |
Spark Streaming Job과의 연결도 확인합니다.
Kafka 내부 주소 (Spark에서 사용)
kafka-cluster-a-kafka-bootstrap.kafka.svc.cluster.local:9092
즉, 여기서 확인할 수 있는 핵심은
Kafka READY 상태 + KafkaTopic READY 상태 + AKHQ에서 토픽이 보이면 Kafka 클러스터 구축이 완료된 것이라는 점입니다.
📝 마무리
이번 글에서는
Kubernetes 위에서 Strimzi Operator를 활용해
KRaft 모드 Kafka 클러스터를 구축하는 과정을 정리해 봤습니다.
정리하면,
- Strimzi Operator → kustomize로 설치, Kafka 클러스터를 Kubernetes 리소스로 관리
- KRaft 모드 → Zookeeper 없이 브로커 자체가 메타데이터 관리
- KafkaNodePool → pool-a, pool-b로 브로커를 풀 단위로 분리 운영
- LocalPath StorageClass → 로컬 디스크 직접 사용으로 쓰기 레이턴시 최소화
- AKHQ → 토픽, 메시지, 컨슈머 그룹 UI 모니터링
라고 볼 수 있습니다.
특히 이번 구축에서 주의해야 할 포인트는
- kustomize namespace 치환 설정 필수 (RBAC 오류 방지)
- Kafka READY 확인 후 → KafkaTopic apply
- podAntiAffinity는 preferred 사용 (노드 2대에 브로커 4개 배포)
- auto.create.topics.enable: false → 토픽은 반드시 KafkaTopic으로 명시적 생성
- /data/kafka-storage 경로 사전 생성 필수
이 5가지였습니다.
다음 편에서는
이번에 구축한 Kafka 위에서 Trino를 연동해서 Delta Lake 데이터를 SQL로 조회하는 방법을 다뤄보겠습니다. 🙌
궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있다면 언제든지 편하게 말씀해 주세요! 😊