
Intro
안녕하세요. 환이s입니다 👋
최근 사내에서 데이터 레이크하우스 플랫폼을 직접 구축하는 프로젝트를 진행했습니다.
단순히 데이터를 쌓는 것에서 끝나는 게 아니라,
수집 → 저장 → 처리 → 분석 → 시각화까지 이어지는
전체 데이터 파이프라인을
오픈소스 기반으로 직접 설계하고 올려보는 작업이었는데요.
이번 시리즈에서는
그 과정에서 구축한 스택들을 하나씩 순서대로 정리해보려고 합니다.
전체 구성은 아래와 같습니다.
| 역할 | 오픈소스 |
| 분산 스토리지 | Ceph |
| 스트리밍 파이프라인 | Kafka |
| 대규모 데이터 처리 | Spark |
| 쿼리 엔진 | Trino |
| 워크플로우 오케스트레이션 | Airflow |
| 인프라 모니터링 | Prometheus / Grafana |
| 데이터 시각화 | Superset |
그 첫 번째 주제로,
모든 데이터가 실제로 저장되는 기반 스토리지인 Rook-Ceph 구축부터 시작하겠습니다.
Kubernetes 위에서 Ceph를 운영하다 보면
단순히 디스크를 마운트하는 것에서 끝나는 게 아니라,
Block / FileSystem / Object Store
세 가지 스토리지 타입을 상황에 맞게 골라 써야 하고,
이후 Spark, Trino, Delta Lake 같은
컴포넌트들이 이 스토리지 위에서 데이터를 읽고 쓰는 구조가 됩니다.
이번 글에서는
- Rook-Ceph 전체 아키텍처가 어떻게 구성되는지
- 실제로 어떤 순서로 설치하면 되는지
- Block / FileSystem / Object Store 각각을 어떻게 구성하는지
- S3 외부 접근까지 어떻게 연결하는지
까지 순서대로 확인해 보겠습니다.
1. Rook-Ceph 아키텍처 개요

본격적인 설치에 앞서,
Rook-Ceph가 어떤 구조로 동작하는지 먼저 이해하는 것이 중요합니다.
Ceph는 크게 아래 5가지 핵심 컴포넌트로 구성됩니다.
| 컴포넌트 | 역할 |
| MON (Monitor) | 클러스터 상태 및 맵 관리, 홀수 개 운영 권장 |
| MGR (Manager) | 메트릭 수집, Dashboard, 모듈 관리 |
| OSD (Object Storage Daemon) | 실제 데이터를 저장하는 디스크 단위 데몬 |
| MDS (Metadata Server) | CephFS 파일시스템의 메타데이터 관리 |
| RGW (RADOS Gateway) | S3 / Swift 호환 오브젝트 스토리지 API 제공 |
Rook은 이 Ceph 컴포넌트들을
Kubernetes의 Custom Resource로 선언하면 자동으로 배포하고 관리해주는 오퍼레이터입니다.
즉, 여기서 확인할 수 있는 핵심은
복잡한 Ceph 운영을 Kubernetes 선언형 방식으로 관리할 수 있다는 점입니다.
2. 사전 준비: 노드 디스크와 레이블 확인
Rook-Ceph는 OSD가 올라갈 노드에 미포맷 상태의 빈 디스크가 있어야 합니다.
이번 구성에서는 worker3, worker4, worker5 노드의 vdb 디스크를 사용합니다.
디스크 상태 확인은 각 노드에서 아래 명령어로 할 수 있습니다.
lsblk
정상적인 상태라면 아래처럼 vdb가 마운트 없이 보여야 합니다.
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
vda 252:0 0 50G 0 disk
└─vda1 252:1 0 50G 0 part /
vdb 252:16 0 100G 0 disk ← 마운트 없음
그다음 OSD 노드에 레이블을 설정합니다.
kubectl label node k8s-worker3 role=ceph
kubectl label node k8s-worker4 role=ceph
kubectl label node k8s-worker5 role=ceph
레이블 확인은 아래 명령어로 할 수 있습니다.
kubectl get nodes --show-labels | grep ceph
즉, 여기서 확인할 수 있는 핵심은
OSD 노드는 빈 디스크와 레이블이 사전에 준비되어 있어야 Rook이 정상적으로 OSD를 배포할 수 있다는 점입니다.
3. 설치 순서 개요
이번 구축에서 사용하는 매니페스트 디렉토리 구조는 아래와 같습니다.
manifests/
├── 10_crd # CRD 등록
├── 20_common # Namespace, 공통 RBAC
├── 30_operator # Rook Operator 배포
├── 40_cluster # CephCluster 생성
├── 50_toolbox # 디버깅용 Toolbox + Dashboard 외부 접근
├── 60_object-store # CephFS + Object Store(RGW)
├── 70_object-user # S3 유저 + NodePort Service
├── 80_storageclass # StorageClass 등록
└── 90_rbac # ServiceAccount / Role
설치는 반드시 이 번호 순서대로 진행해야 하며,
중간에 반드시 대기해야 하는 구간이 3곳 있습니다.
10 → 20 → 30 → ⏳ → 40 → ⏳ HEALTH_OK → 50 → 60 → ⏳ → 70 → 80 → 90
각 단계를 순서대로 살펴보겠습니다.
4. crd / common / operator : 기반 리소스 설치
기반 리소스는 대부분 Rook 공식 GitHub에서 제공하는 파일을 그대로 사용하지만,
이번 구성에서는 두 가지를 추가로 커스텀했습니다.
첫 번째는 common 디렉토리에 Namespace 정의를 별도로 추가했습니다.
공식 common.yaml에는 Namespace가 포함되어 있지 않아서 아래 파일을 직접 추가했습니다.
# common에 추가
apiVersion: v1
kind: Namespace
metadata:
name: rook-ceph
두 번째는 operator 디렉토리를 아래처럼 3개 파일로 분리해서 구성했습니다.
operator/
├── csi-operator.yaml # 공식 파일 그대로 사용
├── operator.yaml # 공식 파일 그대로 사용
└── dashboard-secret.yaml # Ceph Dashboard 접근용 Secret (커스텀 추가)
dashboard-secret.yaml은
Ceph Dashboard 로그인에 사용할 패스워드를 Secret으로 등록하는 파일입니다.
apiVersion: v1
kind: Secret
metadata:
name: rook-ceph-dashboard-password
namespace: rook-ceph
type: kubernetes.io/rook
stringData:
password: "admin123"
⚠️ 운영 환경에서는 password를 반드시 변경해서 사용하세요.
순서대로 적용합니다.
kubectl apply -f crd/
kubectl apply -f common/
kubectl apply -f operator/
Operator Pod가 정상적으로 올라왔는지 확인합니다.
kubectl get pods -n rook-ceph -w
아래처럼 Running 상태가 되면 다음 단계로 진행합니다.
NAME READY STATUS RESTARTS AGE
rook-ceph-operator-xxxxxxxxxx-xxxxx 1/1 Running 0 ...
즉, 여기서 확인할 수 있는 핵심은 Namespace가 먼저 생성된 이후에 Operator가 배포되어야 하고,
Operator가 Running 상태가 된 이후에만 CephCluster 리소스를 인식하고 처리할 수 있다는 점입니다.
5. cluster: CephCluster 생성
CephCluster는 Rook-Ceph의 핵심 리소스입니다.
이 yaml 하나로 MON, MGR, OSD, Dashboard 등 전체 클러스터 구성이 결정됩니다.
전체 설정 파일은 아래와 같습니다.
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
name: rook-ceph
namespace: rook-ceph
spec:
cephVersion:
image: harbor.local/library/rook-ceph/ceph:v19.2.3
allowUnsupported: false
cephConfig:
global:
mon_max_pg_per_osd: "300"
dataDirHostPath: /var/lib/rook
skipUpgradeChecks: false
continueUpgradeAfterChecksEvenIfNotHealthy: false
waitTimeoutForHealthyOSDInMinutes: 10
upgradeOSDRequiresHealthyPGs: false
mgr:
count: 2
allowMultiplePerNode: false
modules:
- name: rook
enabled: true
dashboard:
enabled: true
ssl: true
monitoring:
enabled: false
metricsDisabled: false
exporter:
perfCountersPrioLimit: 5
statsPeriodSeconds: 5
network:
connections:
encryption:
enabled: false
compression:
enabled: false
requireMsgr2: false
crashCollector:
disable: false
logCollector:
enabled: true
periodicity: daily
maxLogSize: 500M
cleanupPolicy:
confirmation: ""
sanitizeDisks:
method: quick
dataSource: zero
iteration: 1
allowUninstallWithVolumes: false
mon:
count: 3
allowMultiplePerNode: true
placement:
all:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values:
- "ceph"
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
mon:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values:
- "ceph"
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
removeOSDsIfOutAndSafeToRemove: false
priorityClassNames:
mon: system-node-critical
osd: system-node-critical
mgr: system-cluster-critical
storage:
useAllNodes: false
nodes:
- name: "k8s-worker3"
deviceFilter: "^vdb"
- name: "k8s-worker4"
deviceFilter: "^vdb"
- name: "k8s-worker5"
deviceFilter: "^vdb"
useAllDevices: false
allowDeviceClassUpdate: false
allowOsdCrushWeightUpdate: false
scheduleAlways: false
onlyApplyOSDPlacement: false
# fullRatio: 0.95
# backfillFullRatio: 0.90
# nearFullRatio: 0.85
disruptionManagement:
managePodBudgets: true
osdMaintenanceTimeout: 30
csi:
readAffinity:
enabled: false
healthCheck:
daemonHealth:
mon:
disabled: false
interval: 45s
osd:
disabled: false
interval: 60s
status:
disabled: false
interval: 60s
livenessProbe:
mon:
disabled: false
mgr:
disabled: false
osd:
disabled: false
startupProbe:
mon:
disabled: false
mgr:
disabled: false
osd:
disabled: false
주요 설정 포인트를 섹션별로 살펴보겠습니다.
Ceph 버전
cephVersion:
image: harbor.local/library/rook-ceph/ceph:v19.2.3
공식 quay.io 대신 내부 Harbor 레지스트리에서 이미지를 가져오도록 설정했습니다.
에어갭(Air-gap) 환경이나 사내 네트워크 환경에서는 이처럼 내부 레지스트리를 사용하는 것이 일반적입니다.
MON / MGR
mon:
count: 3
allowMultiplePerNode: true # 테스트 환경에서 유연성 확보
mgr:
count: 2 # MGR 이중화로 가용성 향상
allowMultiplePerNode: false
MON은 항상 홀수 개로 운영해야 quorum이 유지됩니다.
MGR은 2개로 설정해서 Active/Standby 구성을 갖췄습니다.
Dashboard
dashboard:
enabled: true
ssl: true
Dashboard를 활성화하고 SSL을 적용했습니다.
앞서 operator에서 등록한 rook-ceph-dashboard-password Secret이 이 Dashboard 로그인에 사용됩니다.
Monitoring
monitoring:
enabled: false # 현재는 비활성화
현재는 비활성화 상태입니다.
이후 Prometheus 구축 편에서 enabled: true로 변경해서 메트릭 연동을 활성화할 예정입니다.
Placement
placement:
all:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values:
- "ceph"
role=ceph 레이블이 있는 노드에만 Ceph 컴포넌트가 배포되도록 강제했습니다.
사전 준비 단계에서 worker3, 4, 5에 레이블을 붙인 이유가 바로 이 설정 때문입니다.
Storage
storage:
useAllNodes: false
nodes:
- name: "k8s-worker3"
deviceFilter: "^vdb"
- name: "k8s-worker4"
deviceFilter: "^vdb"
- name: "k8s-worker5"
deviceFilter: "^vdb"
useAllNodes: false로 설정해서 의도하지 않은 노드에 OSD가 올라가는 것을 방지하고,
각 노드의 vdb 디바이스만 OSD로 사용하도록 명시했습니다.
💡 운영 환경 팁: 현재 주석 처리된 아래 값들은 디스크 용량 관리에 중요한 설정입니다. 운영 환경에서는 명시적으로 설정하는 것을 권장합니다.
# fullRatio: 0.95 # IO 차단 임계값 # backfillFullRatio: 0.90 # 백필 중단 임계값 # nearFullRatio: 0.85 # 경고 발생 임계값
이제 적용합니다.
kubectl apply -f cluster/
클러스터 생성은 5~10분 정도 소요됩니다.
아래 명령어로 Pod 상태를 확인합니다.
kubectl get pods -n rook-ceph -w
정상적으로 올라오면 아래처럼 보입니다.
rook-ceph-mon-a-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-mon-b-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-mon-c-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-mgr-a-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-mgr-b-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-osd-0-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-osd-1-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-osd-2-xxxxxxxxxx 1/1 Running 0 ...
즉, 여기서 확인할 수 있는 핵심은
MON 3개 + MGR 2개 + OSD 3개가 모두 Running 상태여야 다음 단계로 진행할 수 있다는 점입니다.
6. toolbox: Ceph 상태 확인 및 Dashboard 외부 접근 설정
toolbox 디렉토리에는
단순 디버깅 도구뿐 아니라 Dashboard를 외부에서 접근하기 위한 설정도 함께 담겨 있습니다.
50_toolbox/
├── toolbox.yaml # Toolbox Deployment (디버깅용)
├── dashboard-external-https.yaml # Dashboard NodePort Service
└── ceph-dashboard-ingress.yaml # Dashboard Ingress
toolbox: 디버깅용 Pod
Toolbox는 Ceph 클러스터 내부를 직접 조회할 수 있는 디버깅용 Deployment입니다.
내부적으로 MON 엔드포인트와
admin keyring을 자동으로 읽어와 별도 설정 없이 ceph 명령어를 바로 사용할 수 있습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: rook-ceph-tools
namespace: rook-ceph
labels:
app: rook-ceph-tools
spec:
replicas: 1
selector:
matchLabels:
app: rook-ceph-tools
template:
spec:
dnsPolicy: ClusterFirstWithHostNet
serviceAccountName: rook-ceph-default
containers:
- name: rook-ceph-tools
image: harbor.local/library/rook-ceph/ceph:v19
securityContext:
runAsNonRoot: true
runAsUser: 2016
runAsGroup: 2016
capabilities:
drop: ["ALL"]
env:
- name: ROOK_CEPH_USERNAME
valueFrom:
secretKeyRef:
name: rook-ceph-mon
key: ceph-username
volumeMounts:
- mountPath: /etc/ceph
name: ceph-config
- name: mon-endpoint-volume
mountPath: /etc/rook
- name: ceph-admin-secret
mountPath: /var/lib/rook-ceph-mon
readOnly: true
- name: rook-config-override
mountPath: /etc/rook-config-override
readOnly: true
volumes:
- name: ceph-admin-secret
secret:
secretName: rook-ceph-mon
items:
- key: ceph-secret
path: secret.keyring
- name: mon-endpoint-volume
configMap:
name: rook-ceph-mon-endpoints
items:
- key: data
path: mon-endpoints
- name: rook-config-override
configMap:
name: rook-config-override
optional: true
- name: ceph-config
emptyDir: {}
Toolbox Pod에 접속해서 클러스터 상태를 확인합니다.
kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
정상적인 경우 아래처럼 HEALTH_OK가 출력됩니다.
cluster:
id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
health: HEALTH_OK
services:
mon: 3 daemons, quorum a,b,c
mgr: a(active), b(standby)
osd: 3 osds: 3 up, 3 in
OSD 상세 상태도 같이 확인해 두면 좋습니다.
kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph osd status
+----+-------------+-------+-------+--------+---------+
| id | host | used | avail | wr ops | wr data |
+----+-------------+-------+-------+--------+---------+
| 0 | k8s-worker3 | ... | ... | 0 | 0 |
| 1 | k8s-worker4 | ... | ... | 0 | 0 |
| 2 | k8s-worker5 | ... | ... | 0 | 0 |
+----+-------------+-------+-------+--------+---------+
HEALTH_OK를 반드시 확인한 이후에 다음 단계로 진행해야 합니다.
dashboard-external-https: Dashboard NodePort 노출
Ceph Dashboard를
클러스터 외부에서 접근할 수 있도록 NodePort Service를 생성합니다.
apiVersion: v1
kind: Service
metadata:
name: rook-ceph-mgr-dashboard-external-https
namespace: rook-ceph
labels:
app: rook-ceph-mgr
rook_cluster: rook-ceph
spec:
ports:
- name: dashboard
port: 8443
protocol: TCP
targetPort: 8443
nodePort: 30510
selector:
app: rook-ceph-mgr
mgr_role: active # Active MGR Pod에만 트래픽 전달
rook_cluster: rook-ceph
sessionAffinity: None
type: NodePort
mgr_role: active 셀렉터가 핵심입니다.
MGR이 2개 떠있을 때 Active 상태인 MGR Pod에만 트래픽이 전달됩니다.
ceph-dashboard-ingress: Ingress로 도메인 접근
NodePort 대신 도메인으로 접근할 수 있도록 Ingress도 함께 설정했습니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ceph-dashboard
namespace: rook-ceph
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
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: ceph.192.168.2.240.sslip.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: rook-ceph-mgr-dashboard-external-https
port:
number: 8443
backend-protocol: "HTTPS" 어노테이션이 중요한 포인트입니다.
Dashboard가 SSL로 서빙되기 때문에 Ingress가 백엔드로 HTTPS 통신을 해야 합니다.
이 어노테이션이 없으면 Ingress가 HTTP로 백엔드에 접근해서 연결이 실패합니다.
Dashboard 접근은 아래처럼 확인할 수 있습니다.
# NodePort로 직접 접근
https://<노드IP>:30510
# Ingress 도메인으로 접근
https://ceph.192.168.2.240.sslip.io
로그인 시 operator에서 등록한 Secret의 패스워드를 사용합니다.
ID: admin
PW: admin123 (dashboard-secret.yaml에서 설정한 값)
즉, 여기서 확인할 수 있는 핵심은 Toolbox로 HEALTH_OK를 확인하고,
Dashboard로 클러스터 상태를 시각적으로 모니터링하는 환경까지 이 단계에서 함께 구성된다는 점입니다.
7. object-store: CephFS와 Object Store(RGW) 구성
object-store 디렉토리에는 두 가지가 포함되어 있습니다.
- CephFilesystem (MDS): ReadWriteMany PVC용 파일시스템
- CephObjectStore (RGW): S3 호환 오브젝트 스토리지
CephFilesystem 설정
CephFilesystem은
여러 Pod가 동시에 마운트 할 수 있는 ReadWriteMany 방식의 공유 파일시스템을 제공합니다.
apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
name: myfs
namespace: rook-ceph
spec:
metadataPool:
replicated:
size: 3
requireSafeReplicaSize: true
parameters:
compression_mode: none
dataPools:
- name: replicated
failureDomain: host
replicated:
size: 3
requireSafeReplicaSize: true
parameters:
compression_mode: none
preserveFilesystemOnDelete: true
metadataServer:
activeCount: 1
activeStandby: true
placement:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: role
operator: In
values:
- "ceph"
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- rook-ceph-mds
topologyKey: kubernetes.io/hostname
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- rook-ceph-mds
topologyKey: topology.kubernetes.io/zone
priorityClassName: system-cluster-critical
livenessProbe:
disabled: false
startupProbe:
disabled: false
---
apiVersion: ceph.rook.io/v1
kind: CephFilesystemSubVolumeGroup
metadata:
name: myfs-csi
namespace: rook-ceph
spec:
name: csi
filesystemName: myfs
pinning:
distributed: 1
주요 설정 포인트는 아래와 같습니다.
metadataPool / dataPools
replicated:
size: 3
requireSafeReplicaSize: true # replica 1 설정 방지 (데이터 손실 예방)
failureDomain: host # 호스트 단위로 replica 분산
requireSafeReplicaSize: true는
실수로 replica를 1로 설정하는 것을 방지하는 안전장치입니다.
metadataServer
activeCount: 1 # Active MDS 1개
activeStandby: true # Standby MDS도 1개 → 총 2개 MDS Pod 생성
activeStandby: true로 설정하면
Standby MDS가 메타데이터 캐시를 미리 워밍업 해두어 Active MDS 장애 시 빠르게 전환됩니다.
preserveFilesystemOnDelete
preserveFilesystemOnDelete: true # CephFilesystem 삭제 시 실제 데이터 보존
CephObjectStore의
preservePoolsOnDelete: true와 같은 개념으로 리소스를 실수로 삭제해도 데이터가 보존됩니다.
SubVolumeGroup
kind: CephFilesystemSubVolumeGroup
spec:
name: csi
filesystemName: myfs
pinning:
distributed: 1 # MDS 부하를 여러 데몬에 분산
CSI 드라이버가 CephFS를 사용할 때 SubVolumeGroup 단위로 볼륨을 관리합니다.
distributed: 1로 설정하면 MDS 부하가 여러 데몬에 자동으로 분산됩니다.
MDS Pod 상태를 확인합니다.
kubectl get pods -n rook-ceph | grep mds
activeStandby: true로 설정했기 때문에 MDS Pod가 2개 뜹니다.
rook-ceph-mds-myfs-a-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-mds-myfs-b-xxxxxxxxxx 1/1 Running 0 ...
CephObjectStore 설정
apiVersion: ceph.rook.io/v1
kind: CephObjectStore
metadata:
name: ceph-store
namespace: rook-ceph
spec:
metadataPool:
failureDomain: host
replicated:
size: 3
dataPool:
failureDomain: host
replicated:
size: 3
preservePoolsOnDelete: true
gateway:
port: 80
instances: 2
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
주요 설정 포인트는 아래와 같습니다.
- failureDomain: host → 호스트 단위로 replica를 분산해서 노드 장애 시에도 데이터 보존
- preservePoolsOnDelete: true → CephObjectStore 삭제 시 Pool은 보존 (데이터 보호)
- instances: 2 → RGW Pod를 2개 띄워 이중화
RGW Pod가 정상적으로 올라왔는지 확인합니다.
kubectl get pods -n rook-ceph | grep rgw
instances: 2로 설정했기 때문에 RGW Pod가 2개 뜹니다.
rook-ceph-rgw-ceph-store-a-xxxxxxxxxx 1/1 Running 0 ...
rook-ceph-rgw-ceph-store-b-xxxxxxxxxx 1/1 Running 0 ...
즉, 여기서 확인할 수 있는 핵심은
CephFilesystem은 MDS Pod 2개(Active + Standby)가, CephObjectStore는 RGW Pod 2개가 뜨면서 각각 이중화 구성이 완료된다는 점입니다.
그리고 RGW Pod가 완전히 Running 상태가 된 이후에만 다음 단계인 S3 유저 생성으로 진행해야 합니다.
8. object-user: S3 유저 생성 및 외부 노출
RGW가 Running 상태가 된 이후에 S3 유저를 생성합니다.
이번 구성에서는 Delta Lake용 유저를 생성합니다.
Iceberg 유저는 이후 Spark + Iceberg 연동 편에서 활성화할 예정입니다.
apiVersion: ceph.rook.io/v1
kind: CephObjectStoreUser
metadata:
name: delta-user
namespace: rook-ceph
spec:
store: ceph-store
displayName: "Delta Lake Spark User"
S3 엔드포인트는 NodePort로 외부에 노출합니다.
apiVersion: v1
kind: Service
metadata:
name: rook-ceph-rgw-ceph-store-external
namespace: rook-ceph
labels:
app: rook-ceph-rgw
rook_cluster: rook-ceph
rook_object_store: ceph-store
spec:
ports:
- name: rgw
port: 80
protocol: TCP
targetPort: 8080
nodePort: 30511
selector:
app: rook-ceph-rgw
ceph_daemon_id: ceph-store
rgw: ceph-store
rook_cluster: rook-ceph
rook_object_store: ceph-store
sessionAffinity: None
type: NodePort
kubectl apply -f object-user/
유저가 생성되면 Rook이 자동으로 Secret을 만들어 줍니다.
아래 명령어로 AccessKey와 SecretKey를 확인할 수 있습니다.
# AccessKey 확인
kubectl get secret rook-ceph-object-user-ceph-store-delta-user \
-n rook-ceph -o jsonpath='{.data.AccessKey}' | base64 --decode
# SecretKey 확인
kubectl get secret rook-ceph-object-user-ceph-store-delta-user \
-n rook-ceph -o jsonpath='{.data.SecretKey}' | base64 --decode
S3 접근 테스트는 아래처럼 할 수 있습니다.
aws s3 ls \
--endpoint-url http://<노드IP>:30511 \
--access-key <AccessKey> \
--secret-key <SecretKey> \
--no-verify-ssl
즉, 여기서 확인할 수 있는 핵심은
RGW Pod가 완전히 Running 상태가 된 이후에
CephObjectStoreUser를 생성해야 Secret이 정상적으로 만들어지고,
이 Secret 안의 AccessKey / SecretKey가 이후 Spark, Delta Lake 연동에서 S3 인증 정보로 사용된다는 점입니다.
9. storageclass : 스토리지 타입별 StorageClass 등록
이번 구성에서는 Block(RBD)과 CephFS 두 가지 StorageClass를 등록합니다.
Block StorageClass (RWO)
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
name: replicapool
namespace: rook-ceph
spec:
failureDomain: host
replicated:
size: 3
requireSafeReplicaSize: true
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
clusterID: rook-ceph
pool: replicapool
imageFormat: "2"
imageFeatures: layering
csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-publish-secret-name: rook-csi-rbd-provisioner
csi.storage.k8s.io/controller-publish-secret-namespace: rook-ceph
csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
csi.storage.k8s.io/fstype: ext4
allowVolumeExpansion: true
reclaimPolicy: Delete
여기서 imageFeatures: layering만 사용한 이유는
worker 노드 커널 버전이 5.4 미만인 경우 fast-diff, object-map 등 고급 기능을 지원하지 않기 때문입니다.
커널 버전은 아래 명령어로 확인할 수 있습니다.
uname -r
커널이 5.4 이상이라면 아래처럼 변경하면 성능이 더 좋습니다.
# 커널 5.4 이상인 경우
imageFeatures: layering,fast-diff,object-map,deep-flatten,exclusive-lock
CephFS StorageClass (RWX)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rook-cephfs
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
clusterID: rook-ceph
fsName: myfs
pool: myfs-replicated
csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-publish-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/controller-publish-secret-namespace: rook-ceph
csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
allowVolumeExpansion: true
reclaimPolicy: Delete
mountOptions: []
kubectl apply -f storageclass/
kubectl get storageclass
아래처럼 두 가지 StorageClass가 등록된 것을 확인할 수 있습니다.
NAME PROVISIONER RECLAIMPOLICY AGE
rook-ceph-block rook-ceph.rbd.csi.ceph.com Delete ...
rook-cephfs rook-ceph.cephfs.csi.ceph.com Delete ...
즉, 여기서 확인할 수 있는 핵심은
두 StorageClass의 차이는 접근 모드에 있다는 점입니다.
| StorageClass | 접근 모드 | 주 용도 |
| rook-ceph-block | RWO (단일 노드) | DB, 단일 Pod 스토리지 |
| rook-cephfs | RWX (다중 노드) | 공유 파일시스템, 여러 Pod 동시 마운트 |
그리고 현재 두 StorageClass 모두
reclaimPolicy: Delete로 설정되어 있어서 PVC를 삭제하면 데이터도 함께 삭제됩니다.
운영 환경에서 중요한 데이터를 다루는 PVC라면 Retain으로 변경하는 것을 권장합니다.
10. rbac : ServiceAccount 및 권한 설정
마지막으로 Ceph 리소스에 접근할 수 있는 ServiceAccount와 RBAC을 적용합니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: rook-ceph-sa
namespace: rook-ceph
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: rook-ceph-role
namespace: rook-ceph
rules:
- apiGroups: [""]
resources:
- pods
- pods/log
- services
- configmaps
- secrets
- events
- persistentvolumeclaims
verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"]
- apiGroups: ["ceph.rook.io"]
resources:
- cephclusters
- cephblockpools
- cephobjectstores
- cephfilesystems
- cephobjectstoreusers
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: rook-ceph-rb
namespace: rook-ceph
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: rook-ceph-role
subjects:
- kind: ServiceAccount
name: rook-ceph-sa
namespace: rook-ceph
---
apiVersion: v1
kind: Secret
metadata:
name: rook-ceph-sa-token
namespace: rook-ceph
annotations:
kubernetes.io/service-account.name: rook-ceph-sa
type: kubernetes.io/service-account-token
⚠️ 참고: PersistentVolume은 클러스터 전체 범위(Namespace 미적용) 리소스입니다. PV에 직접 접근이 필요한 경우 Role이 아닌 ClusterRole을 별도로 추가해야 합니다.
# PV 접근이 필요한 경우 별도 추가
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rook-ceph-clusterrole
rules:
- apiGroups: [""]
resources:
- persistentvolumes
- nodes
- nodes/proxy
verbs: ["get", "list", "watch"]
즉, 여기서 확인할 수 있는 핵심은
Role은 특정 Namespace 안의 리소스만 제어할 수 있고,
PV처럼 클러스터 전체에 속하는 리소스는 ClusterRole을 별도로 만들어야 접근이 가능하다는 점입니다.
11. 전체 구성 검증
모든 설치가 완료된 후 최종 상태를 확인합니다.
# 전체 Pod 상태
kubectl get pods -n rook-ceph
# Ceph 클러스터 전체 상태
kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
# OSD 상태
kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph osd status
# 스토리지 용량
kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph df
정상적인 경우 ceph df 결과는 아래처럼 나옵니다.
--- RAW STORAGE ---
CLASS SIZE AVAIL USED RAW USED %RAW USED
hdd 300 GiB 299 GiB xxx MiB xxx MiB 0.xx
TOTAL 300 GiB 299 GiB xxx MiB xxx MiB 0.xx
--- POOLS ---
POOL ID PGS STORED OBJECTS USED %USED
replicapool 1 32 ... ... ... ...
myfs-replicated 2 32 ... ... ... ...
.rgw.root 3 8 ... ... ... ...
즉, 여기서 확인할 수 있는 핵심은
ceph status의 HEALTH_OK와 각 Pool이 정상적으로 등록된 것이 확인되면 스토리지 구축이 완료된 것이라는 점입니다.
📝 마무리
이번 글에서는 Kubernetes 위에서
Rook-Ceph를 활용해 분산 스토리지를 직접 구축하는 과정을 순서대로 정리해 봤습니다.
정리하면,
- Rook Operator가 Ceph 컴포넌트를 Kubernetes 리소스로 관리
- Block(RBD) → 단일 Pod 전용 RWO 스토리지
- CephFS → 여러 Pod가 동시에 마운트 가능한 RWX 스토리지
- Object Store(RGW) → S3 호환 API로 접근하는 오브젝트 스토리지
라고 볼 수 있습니다.
특히 이번 구축에서 주의해야 할 포인트는
- Operator Running 확인 후 → Cluster 적용
- HEALTH_OK 확인 후 → Object Store 적용
- RGW Running 확인 후 → S3 유저 생성
이 3가지 대기 구간을 반드시 지켜야 한다는 점이었습니다.
다음 편에서는
이번에 구축한 Ceph Object Store를 Spark + Delta Lake 연동에서 실제로 사용하는 방법을 다뤄보겠습니다. 🙌
궁금한 점이나 추가로 다뤄줬으면 하는 내용이 있다면 언제든지 편하게 말씀해 주세요! 😊
'[ 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] Apache Spark 구축 가이드(Spark Operator + Delta Lake) (1) | 2026.06.03 |