
Intro
안녕하세요. 환이s입니다 👋
지난 글에서는
데이터 레이크하우스 플랫폼 기반 스토리지인 Rook-Ceph를 Kubernetes 위에 구축하는 과정을 정리해 봤습니다.
이번 글에서는
그 위에 올라가는 두 번째 스택,
Apache Spark를 Kubernetes 환경에 구축하는 과정을 정리해보려고 합니다.

Spark를 Kubernetes 위에서 운영하다 보면
단순히 Job을 실행하는 것에서 끝나는 게 아니라,
- Operator가 SparkApplication 리소스를 감시하고 Driver/Executor Pod를 자동으로 관리하고
- 실행된 Job의 이벤트 로그가 Ceph S3에 저장되고
- History Server가 그 로그를 읽어서 실행 이력을 보여주고
- Delta Lake 포맷으로 Ceph S3에 데이터를 읽고 쓰는
전체 흐름이 유기적으로 연결되어야 합니다.
이번 글에서는
- spark-operator 아키텍처가 어떻게 동작하는지
- 실제로 어떤 순서로 설치하면 되는지
- Delta Lake + Ceph S3 연동을 어떻게 구성하는지
- Spark History Server와 File Browser까지 어떻게 구성하는지
까지 순서대로 확인해 보겠습니다.
1. spark-operator 아키텍처 개요
본격적인 설치에 앞서,
spark-operator가 어떤 구조로 동작하는지 먼저 이해하는 것이 중요합니다.
spark-operator는
Kubernetes의 Custom Resource를 활용해서
Spark Job을 선언형으로 관리할 수 있게 해주는 오퍼레이터입니다.
동작 흐름은 아래와 같습니다.
사용자가 SparkApplication yaml을 apply
↓
spark-operator가 SparkApplication 리소스 감지
↓
Driver Pod 생성
↓
Driver가 Executor Pod 생성 요청
↓
Executor Pod 생성 → Job 실행
↓
이벤트 로그 → Ceph S3 저장
↓
History Server → S3 로그 읽어서 UI 제공
이번 구성에서 사용하는 주요 CRD는 아래 3가지입니다.
| CRD | 역할 |
| SparkApplication | 단발성 Spark Job 정의 |
| ScheduledSparkApplication | 주기적으로 실행되는 Spark Job 정의 |
| SparkConnect | Spark Connect 서버 정의 |
즉, 여기서 확인할 수 있는 핵심은 spark-operator를 사용하면
복잡한 Spark Job 관리를 Kubernetes 리소스 선언 방식으로 단순화할 수 있다는 점입니다.
2. 설치 순서
이번 구축에서 사용하는 매니페스트 디렉토리 구조는 아래와 같습니다.
manifests/
├── crds/ # SparkApplication CRD 등록 (공식 파일 그대로 사용)
├── spark/ # Namespace 설정
├── operator/ # Spark Operator 설치
├── rbac/ # Job 실행용 RBAC + S3 Secret
├── history/ # Spark History Server
└── streaming/ # Delta Lake Streaming Job + File Browser + PVC
설치 순서는 아래와 같습니다.
crds → spark(namespace) → operator → ⏳ → rbac → streaming(PVC/FileBrowser) → history → streaming(Job)
주의해야 할 포인트는 두 가지입니다.
첫 번째로 Operator Pod가 Running 상태가 된 이후에
SparkApplication을 apply해야 정상적으로 Job이 실행됩니다.
두 번째로 History Server를 apply하기 전에
Ceph S3에 spark-event-logs 버킷이 미리 생성되어 있어야 합니다.
버킷이 없으면 History Server가 기동 시 로그 디렉토리를 찾지 못해 오류가 발생합니다.
# spark-event-logs 버킷 사전 생성
aws s3 mb s3://spark-event-logs \
--endpoint-url http://<노드IP>:30511 \
--no-verify-ssl
3. Namespace 설정
이번 구성에서는 Namespace를 두 개로 분리해서 운영합니다.
# spark_namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: spark
# spark_operator_namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: spark-operator
kubectl apply -f spark/
| Namespace | 용도 |
| spark | SparkApplication Job 실행, History Server, File Browser |
| spark-operator | Operator Deployment, RBAC |
Namespace를 분리하는 이유는
단순히 리소스를 나누는 것에서 끝나지 않습니다.
Operator 설정에서 --namespaces=spark,airflow처럼
감시할 Namespace를 명시적으로 지정할 수 있기 때문에
Operator가 올라가는 공간과 Job이 실행되는 공간을 분리해두면
권한 범위와 감시 범위를 명확하게 관리할 수 있습니다.
즉, 여기서 확인할 수 있는 핵심은
Namespace 분리가 이후 Airflow 등 다른 스택과 연동할 때도
Operator 감시 범위를 유연하게 확장할 수 있는 기반이 된다는 점입니다.
4. CRD 등록
CRD는 Spark Operator 공식 GitHub에서 제공하는 파일을 그대로 사용합니다.
crds/
├── 10_scheduledsparkapplications.yaml
├── 20_sparkapplications.yaml
└── 30_sparkconnects.yaml
공식 파일은 아래 GitHub에서 확인할 수 있습니다.
https://github.com/kubeflow/spark-operator/tree/master/charts/spark-operator-chart/crds
spark-operator/charts/spark-operator-chart/crds at master · kubeflow/spark-operator
Kubernetes operator for managing the lifecycle of Apache Spark applications on Kubernetes. - kubeflow/spark-operator
github.com
kubectl apply -f crds/
CRD가 정상적으로 등록되었는지 확인합니다.
kubectl get crd | grep spark
아래처럼 3개가 등록되면 정상입니다.
scheduledsparkapplications.sparkoperator.k8s.io ...
sparkapplications.sparkoperator.k8s.io ...
sparkconnects.sparkoperator.k8s.io ...
즉, 여기서 확인할 수 있는 핵심은
CRD가 먼저 등록되어야 이후 SparkApplication yaml을 apply 할 때
Kubernetes가 해당 리소스 타입을 인식할 수 있다는 점입니다.
5. Operator 설치
Operator 관련 파일은 아래 순서로 구성되어 있습니다.
operator/
├── 10_spark-operator-account.yaml # ServiceAccount
├── 20_spark-operator-clusterRole.yaml # ClusterRole
├── 30_spark-operator-clusterRoleBinding.yaml
├── 40_spark-operator-deployment.yaml # Operator Deployment
├── 50_spark-operator-pdb.yaml # PodDisruptionBudget
└── 60_spark-operator-service.yaml # metrics / health Service
ServiceAccount / ClusterRole / ClusterRoleBinding
apiVersion: v1
kind: ServiceAccount
metadata:
name: spark-operator
namespace: spark-operator
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: spark-operator
rules:
- apiGroups: ["sparkoperator.k8s.io"]
resources:
- sparkapplications
- sparkapplications/status
- scheduledsparkapplications
- scheduledsparkapplications/status
- sparkconnects
- sparkconnects/status
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources:
- pods
- pods/log
- services
- configmaps
- secrets
- events
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: spark-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: spark-operator
subjects:
- kind: ServiceAccount
name: spark-operator
namespace: spark-operator
ClusterRole을 사용하는 이유는
Operator가 spark, airflow 등 여러 Namespace의 SparkApplication 리소스를 동시에 감시해야 하기 때문입니다.
Operator Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-operator
namespace: spark-operator
labels:
app: spark-operator
spec:
replicas: 2
revisionHistoryLimit: 10
selector:
matchLabels:
app: spark-operator
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
template:
metadata:
labels:
app: spark-operator
spec:
serviceAccountName: spark-operator
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: spark-operator
containers:
- name: controller
image: harbor.local/library/spark-operator-controller:2.4.0
imagePullPolicy: IfNotPresent
args:
- controller
- start
- --namespaces=spark,airflow
- --leader-election=true
- --leader-election-lock-namespace=spark-operator
- --leader-election-lock-name=spark-operator-lock
- --enable-metrics=true
- --metrics-bind-address=:8080
- --health-probe-bind-address=:8081
ports:
- name: metrics
containerPort: 8080
- name: health
containerPort: 8081
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
startupProbe:
httpGet:
path: /healthz
port: 8081
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
주요 설정 포인트는 아래와 같습니다.
HA 구성
replicas: 2
--leader-election=true
--leader-election-lock-namespace=spark-operator
Operator를 2개 띄우고
Leader Election을 활성화해서 한쪽이 장애가 나도 나머지가 즉시 Active로 전환됩니다.
다중 Namespace 감시
--namespaces=spark,airflow
spark와 airflow 두 Namespace의 SparkApplication을 동시에 감시합니다.
이후 Airflow 편에서 SparkApplication을 트리거할 때 이 설정이 필요합니다.
podAntiAffinity
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
Operator Pod 2개가 서로 다른 노드에 배치되도록 유도합니다.
Probe 설정
startupProbe:
failureThreshold: 30 # 최대 150초(5s * 30)까지 스타트업 허용
livenessProbe:
initialDelaySeconds: 30
failureThreshold: 6
startupProbe의 failureThreshold: 30은
컨테이너가 완전히 뜨기 전에 livenessProbe가 먼저 실패해서 Pod가 재시작되는 것을 방지합니다.
PodDisruptionBudget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: spark-operator-pdb
namespace: spark-operator
spec:
minAvailable: 1
selector:
matchLabels:
app: spark-operator
노드 드레인이나 업그레이드 시에도
Operator Pod가 최소 1개는 항상 Running 상태를 유지하도록 보장합니다.
Service (metrics / health)
apiVersion: v1
kind: Service
metadata:
name: spark-operator
namespace: spark-operator
spec:
selector:
app: spark-operator
ports:
- name: health
port: 8081
targetPort: 8081
- name: metrics
port: 8080
targetPort: 8080
이후 Prometheus 구축 편에서
이 Service의 metrics 포트를 통해 Spark Operator 메트릭을 수집할 예정입니다.
kubectl apply -f operator/
Operator Pod가 정상적으로 올라왔는지 확인합니다.
kubectl get pods -n spark-operator -w
아래처럼 2개가 Running 상태가 되면 다음 단계로 진행합니다.
NAME READY STATUS RESTARTS AGE
spark-operator-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
spark-operator-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
즉, 여기서 확인할 수 있는 핵심은
Operator Pod 2개가 모두 Running 상태가 된 이후에만
SparkApplication 리소스를 정상적으로 처리할 수 있다는 점입니다.
6. RBAC 설정 : spark-sa와 S3 Secret
spark-sa ClusterRole 설정
# spark_job_rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: spark-sa
namespace: spark
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: spark-driver-role
rules:
- apiGroups: [""]
resources:
- pods
- pods/log
- services
- configmaps
- secrets
- events
- persistentvolumeclaims
verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"]
- apiGroups: ["sparkoperator.k8s.io"]
resources:
- sparkapplications
- sparkapplications/status
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: spark-driver-rb
subjects:
- kind: ServiceAccount
name: spark-sa
namespace: spark
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: spark-driver-role
⚠️ spark-sa를 ClusterRole로 설정한 이유
처음에는 일반 Role로 구성했으나, Driver Pod가 실행 중에 SparkApplication 리소스를 조회하려고 할 때 아래와 같은 403 에러가 발생했습니다.
User "system:serviceaccount:spark:spark-sa" cannot get resource "sparkapplications" in API group "sparkoperator.k8s.io"SparkApplication은 클러스터 전체 범위의 리소스이기 때문에 Namespace 범위인 Role로는 접근이 불가능합니다. 이 문제를 해결하기 위해 ClusterRole로 격상했습니다.
S3 Secret 설정
# spark_secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: spark-sa-token
namespace: spark
annotations:
kubernetes.io/service-account.name: spark-sa
type: kubernetes.io/service-account-token
💡 운영 환경 팁: 현재 SparkApplication의 sparkConf에 S3 AccessKey / SecretKey가 평문으로 작성되어 있습니다. 운영 환경에서는 아래처럼 Kubernetes Secret으로 분리해서 관리하는 것을 권장합니다.
apiVersion: v1 kind: Secret metadata: name: ceph-s3-credentials namespace: spark stringData: access-key: "YOUR_ACCESS_KEY" secret-key: "YOUR_SECRET_KEY"
kubectl apply -f rbac/
즉, 여기서 확인할 수 있는 핵심은
Driver Pod가 SparkApplication 리소스를 조회하려면 ClusterRole 수준의 권한이 필요하다는 점입니다.
7. Spark History Server 구성
Spark History Server는 완료된 Job의 실행 이력을
Ceph S3에 저장된 이벤트 로그를 읽어서 UI로 제공합니다.
History Server를 apply하기 전에
반드시 Ceph S3에 spark-event-logs 버킷이 생성되어 있어야 합니다.
# 버킷 사전 생성 (Ceph RGW NodePort 사용)
aws s3 mb s3://spark-event-logs \
--endpoint-url http://<노드IP>:30511 \
--no-verify-ssl
# 버킷 생성 확인
aws s3 ls \
--endpoint-url http://<노드IP>:30511 \
--no-verify-ssl
버킷이 확인되면 History Server를 배포합니다.
# spark-history-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-history-server
namespace: spark
spec:
replicas: 1
selector:
matchLabels:
app: spark-history-server
template:
metadata:
labels:
app: spark-history-server
spec:
containers:
- name: spark-history-server
image: harbor.local/library/spark-streaming-custom:3.5.1-v5
command:
- /opt/spark/bin/spark-class
- org.apache.spark.deploy.history.HistoryServer
env:
- name: SPARK_HISTORY_OPTS
value: >-
-Dspark.history.fs.logDirectory=s3a://spark-event-logs/
-Dspark.history.ui.port=18080
-Dspark.history.fs.update.interval=10s
-Dspark.hadoop.fs.s3a.impl=org.apache.hadoop.fs.s3a.S3AFileSystem
-Dspark.hadoop.fs.s3a.endpoint=http://rook-ceph-rgw-ceph-store.rook-ceph.svc:80
-Dspark.hadoop.fs.s3a.access.key=YOUR_ACCESS_KEY
-Dspark.hadoop.fs.s3a.secret.key=YOUR_SECRET_KEY
-Dspark.hadoop.fs.s3a.path.style.access=true
-Dspark.hadoop.fs.s3a.connection.ssl.enabled=false
-Dspark.hadoop.fs.s3a.aws.credentials.provider=org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider
-Dspark.hadoop.fs.s3a.metadatastore.impl=org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore
-Dspark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension
-Dspark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog
ports:
- containerPort: 18080
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 500m
memory: 2Gi
---
# spark-history-service.yaml
apiVersion: v1
kind: Service
metadata:
name: spark-history-server
namespace: spark
spec:
selector:
app: spark-history-server
ports:
- port: 18080
targetPort: 18080
nodePort: 30112
type: NodePort
주요 설정 포인트는 아래와 같습니다.
-Dspark.history.fs.logDirectory=s3a://spark-event-logs/
SparkApplication에서 spark.eventLog.dir로
지정한 경로와 동일해야 합니다. History Server가 이 경로에서 이벤트 로그를 읽어옵니다.
-Dspark.history.fs.update.interval=10s
10초마다 S3에서 새로운 로그를 폴링합니다.
Job이 완료된 후 최대 10초 내에 History Server UI에 반영됩니다.
kubectl apply -f history/
History Server UI는 아래 주소로 접근할 수 있습니다.
http://<노드IP>:30112
즉, 여기서 확인할 수 있는 핵심은
SparkApplication의 spark.eventLog.dir과
History Server의 logDirectory가 동일한 S3 경로를 가리켜야 하고,
해당 버킷이 미리 생성되어 있어야 완료된 Job의 실행 이력이 정상적으로 보인다는 점입니다.
8. File Browser + PVC 구성
File Browser는 CephFS PVC에 저장된
Python 스크립트 파일을 웹 UI로 관리할 수 있는 도구입니다.
# spark-file-browser.yaml
# Spark 스크립트 저장용 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: spark-data-pvc
namespace: spark
spec:
accessModes:
- ReadWriteMany
storageClassName: rook-cephfs
resources:
requests:
storage: 1Gi
---
# File Browser 설정 저장용 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: spark-filebrowser-config-pvc
namespace: spark
spec:
accessModes:
- ReadWriteMany
storageClassName: rook-cephfs
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ceph-explorer
namespace: spark
spec:
replicas: 1
selector:
matchLabels:
app: ceph-explorer
template:
metadata:
labels:
app: ceph-explorer
spec:
securityContext:
runAsUser: 185
fsGroup: 185
containers:
- name: explorer
image: harbor.local/library/filebrowser:latest
ports:
- containerPort: 80
args: ["--database", "/config/database.db", "--noauth"]
volumeMounts:
- name: spark-data
mountPath: /srv/spark
- name: fb-config
mountPath: /config
volumes:
- name: spark-data
persistentVolumeClaim:
claimName: spark-data-pvc
- name: fb-config
persistentVolumeClaim:
claimName: spark-filebrowser-config-pvc
---
apiVersion: v1
kind: Service
metadata:
name: ceph-explorer-svc
namespace: spark
spec:
type: NodePort
ports:
- port: 80
nodePort: 30071
selector:
app: ceph-explorer
주요 포인트는 아래와 같습니다.
PVC storageClassName
storageClassName: rook-cephfs
accessModes:
- ReadWriteMany
CephFS StorageClass를 사용해서 RWX 모드로 PVC를 생성합니다.
Driver Pod와 Executor Pod 모두 동일한 PVC를 동시에 마운트 할 수 있습니다.
SparkApplication과 PVC 연결
SparkApplication에서 이 PVC를 아래처럼 마운트 해서
Driver와 Executor 모두 동일한 Python 스크립트에 접근합니다.
sparkConf:
"spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
"spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"
"spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
"spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"
File Browser UI는 아래 주소로 접근할 수 있습니다.
http://<노드IP>:30071
즉, 여기서 확인할 수 있는 핵심은
CephFS의 RWX 특성 덕분에 Driver / Executor / File Browser 세 곳이 동일한 PVC를 동시에 마운트해서 스크립트를 공유할 수 있다는 점입니다.
9. Delta Lake Streaming Job 실행
이제 실제 SparkApplication을 실행합니다.
이번 구성에서는 Kafka에서 syslog를 읽어서
Delta Lake 포맷으로 Ceph S3에 저장하는 Streaming Job입니다.
# spark-streaming-job.yaml
apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
name: streaming-job-test
namespace: spark
spec:
type: Python
sparkVersion: "3.5.1"
pythonVersion: "3"
mode: cluster
image: "harbor.local/library/spark-streaming-custom:3.5.1-v5"
mainApplicationFile: "local:///opt/scripts/syslog_consumer.py"
restartPolicy:
type: Never
timeToLiveSeconds: 600
sparkConf:
# Delta Lake 설정
"spark.sql.extensions": "io.delta.sql.DeltaSparkSessionExtension"
"spark.sql.catalog.spark_catalog": "org.apache.spark.sql.delta.catalog.DeltaCatalog"
# Ceph S3 접속 설정
"spark.hadoop.fs.s3a.impl": "org.apache.hadoop.fs.s3a.S3AFileSystem"
"spark.hadoop.fs.s3a.endpoint": "http://rook-ceph-rgw-ceph-store.rook-ceph.svc:80"
"spark.hadoop.fs.s3a.access.key": "YOUR_ACCESS_KEY"
"spark.hadoop.fs.s3a.secret.key": "YOUR_SECRET_KEY"
"spark.hadoop.fs.s3a.path.style.access": "true"
"spark.hadoop.fs.s3a.connection.ssl.enabled": "false"
"spark.hadoop.fs.s3a.aws.credentials.provider": "org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider"
# S3Guard 비활성화
"spark.hadoop.fs.s3a.metadatastore.impl": "org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore"
# 이벤트 로그 설정
"spark.eventLog.enabled": "true"
"spark.eventLog.dir": "s3a://spark-event-logs/"
"spark.eventLog.compress": "false"
# S3A 업로드 최적화
"spark.hadoop.fs.s3a.multipart.size": "104857600"
"spark.hadoop.fs.s3a.fast.upload": "true"
"spark.hadoop.fs.s3a.fast.upload.buffer": "bytebuffer"
# 처리량 대비 설정
"spark.hadoop.fs.s3a.threads.max": "50"
"spark.hadoop.fs.s3a.connection.maximum": "100"
# PVC 볼륨 마운트
"spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
"spark.kubernetes.driver.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"
"spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.options.claimName": "spark-data-pvc"
"spark.kubernetes.executor.volumes.persistentVolumeClaim.python-script-vol.mount.path": "/opt/scripts"
driver:
cores: 1
memory: "2g"
memoryOverhead: "1g"
serviceAccount: spark-sa
executor:
cores: 1
instances: 3
memory: "2g"
memoryOverhead: "2g"
주요 설정 포인트를 섹션별로 살펴보겠습니다.
mainApplicationFile
mainApplicationFile: "local:///opt/scripts/syslog_consumer.py"
local://은 Driver Pod 내부 파일 시스템 경로를 가리킵니다.
앞서 PVC를 /opt/scripts에 마운트했기 때문에
File Browser로 업로드한 Python 스크립트를 이 경로에서 바로 실행할 수 있습니다.
Delta Lake 카탈로그 설정
"spark.sql.extensions": "io.delta.sql.DeltaSparkSessionExtension"
"spark.sql.catalog.spark_catalog": "org.apache.spark.sql.delta.catalog.DeltaCatalog"
Delta Lake 포맷을 사용하려면 이 두 설정이 반드시 필요합니다.
Ceph S3 엔드포인트
"spark.hadoop.fs.s3a.endpoint": "http://rook-ceph-rgw-ceph-store.rook-ceph.svc:80"
"spark.hadoop.fs.s3a.path.style.access": "true"
Ceph RGW의 클러스터 내부 서비스 주소를 사용합니다.
path.style.access: true는 Ceph RGW가 가상 호스팅 방식 대신 경로 방식의 S3 URL을 사용하기 때문에 반드시 설정해야 합니다.
S3A 업로드 최적화
"spark.hadoop.fs.s3a.multipart.size": "104857600" # 100MB 단위 멀티파트
"spark.hadoop.fs.s3a.fast.upload": "true"
"spark.hadoop.fs.s3a.threads.max": "50"
"spark.hadoop.fs.s3a.connection.maximum": "100"
Streaming Job은 지속적으로 데이터를 S3에 쓰기 때문에 멀티파트 업로드와 커넥션 풀 설정이 성능에 영향을 줍니다.
Executor 구성
executor:
cores: 1
instances: 3
memory: "2g"
memoryOverhead: "2g"
memoryOverhead는 JVM 외부 메모리(Python 프로세스, 버퍼 등)를 위한 설정입니다.
Python 기반 Streaming Job은 overhead를 넉넉하게 잡는 것이 안정적입니다.
kubectl apply -f streaming/
Job이 정상적으로 실행되는지 확인합니다.
kubectl get sparkapplication -n spark
아래처럼 RUNNING 상태가 되면 정상입니다.
NAME STATUS ATTEMPTS START FINISH AGE
streaming-job-test RUNNING 1 2024-xx-xxTxx:xx:xxZ <nil> ...
Driver와 Executor Pod 상태도 확인합니다.
kubectl get pods -n spark
NAME READY STATUS RESTARTS AGE
streaming-job-test-driver 1/1 Running 0 ...
streaming-job-test-exec-1 1/1 Running 0 ...
streaming-job-test-exec-2 1/1 Running 0 ...
streaming-job-test-exec-3 1/1 Running 0 ...
Spark UI는 아래 NodePort로 실시간 모니터링할 수 있습니다.
# spark-streaming-job-service.yaml
apiVersion: v1
kind: Service
metadata:
name: streaming-job-test-ui-nodeport
namespace: spark
spec:
type: NodePort
selector:
spark-role: driver
spark-app-name: streaming-job-test
ports:
- port: 4040
targetPort: 4040
nodePort: 30111
# Spark UI 접근
http://<노드IP>:30111
즉, 여기서 확인할 수 있는 핵심은
SparkApplication이 RUNNING 상태가 되면
Driver 1개 + Executor 3개가 자동으로 생성되고,
Spark UI에서 Stage / Task / Storage 상태를 실시간으로 확인할 수 있다는 점입니다.
10. 전체 구성 검증
모든 설치가 완료된 후 최종 상태를 확인합니다.
# Operator 상태
kubectl get pods -n spark-operator
# Spark Job / History Server / File Browser 상태
kubectl get pods -n spark
# SparkApplication 상태
kubectl get sparkapplication -n spark
# PVC 상태 확인
kubectl get pvc -n spark
정상적인 경우 아래처럼 보입니다.
# spark-operator namespace
NAME READY STATUS RESTARTS AGE
spark-operator-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
spark-operator-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
# spark namespace
NAME READY STATUS RESTARTS AGE
spark-history-server-xxxxxxxxx 1/1 Running 0 ...
ceph-explorer-xxxxxxxxxx 1/1 Running 0 ...
streaming-job-test-driver 1/1 Running 0 ...
streaming-job-test-exec-1 1/1 Running 0 ...
streaming-job-test-exec-2 1/1 Running 0 ...
streaming-job-test-exec-3 1/1 Running 0 ...
각 UI 접근 주소 정리입니다.
서비스 주소 용도
| 서비스 | 주소 | 용도 |
| Spark UI | http://<노드IP>:30111 | 실행 중인 Job 실시간 모니터링 |
| History Server | http://<노드IP>:30112 | 완료된 Job 실행 이력 조회 |
| File Browser | http://<노드IP>:30071 | Python 스크립트 파일 관리 |
즉, 여기서 확인할 수 있는 핵심은
Operator가 정상 동작하고,
SparkApplication이 RUNNING 상태이며,
History Server UI에서 이벤트 로그가 보이면 Spark + Ceph S3 + Delta Lake 전체 연동이 완료된 것이라는 점입니다.
📝 마무리
이번 글에서는
Kubernetes 위에서 spark-operator를 활용해 Delta Lake 기반 Streaming Job을 구축하는 과정을 정리해 봤습니다.
정리하면,
- spark-operator → SparkApplication 리소스를 감시해서 Driver/Executor Pod 자동 관리
- Delta Lake + Ceph S3 → S3A 프로토콜로 Ceph RGW에 Delta 포맷으로 읽기/쓰기
- History Server → S3 이벤트 로그 기반으로 완료된 Job 이력 조회
- File Browser → CephFS PVC를 통한 Python 스크립트 웹 관리
라고 볼 수 있습니다.
특히 이번 구축에서 주의해야 할 포인트는
- CRD 등록 후 → Operator 설치
- Operator Running 확인 후 → SparkApplication apply
- History Server apply 전 → spark-event-logs 버킷 사전 생성 필수
- spark-sa는 ClusterRole이 필요 (SparkApplication 조회 권한 때문)
- S3 path.style.access: true 설정 필수 (Ceph RGW 연동 시)
이 5가지였습니다.
다음 편에서는 이번에 구축한 Spark 위에서 Kafka 스트리밍 파이프라인을 연결하는 방법을 다뤄보겠습니다. 🙌
궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있다면 언제든지 편하게 말씀해 주세요! 😊
'[ Infra ] > Data Platform' 카테고리의 다른 글
| [Data Platform] Prometheus + Grafana 구축 가이드 (feat. kube-prometheus-stack) (0) | 2026.06.09 |
|---|---|
| [Data Platform] Airflow 구축 가이드 (feat. Spark on Kubernetes) (0) | 2026.06.08 |
| [Data Platform] Trino 구축 가이드 (feat. Hive Metastore + Delta Lake) (0) | 2026.06.05 |
| [Data Platform] Apache Kafka 구축 가이드 (feat. KRaft 모드) (0) | 2026.06.04 |
| [Data Platform] Rook-Ceph 구축 가이드(Block / FileSystem / Object Store) (0) | 2026.06.02 |