2025년 11월 7일
단일 노드 한계를 넘어! RBD/CephFS/RGW 세 가지 스토리지를 모두 구축하다
Day 6에서 Prometheus + Grafana로 모니터링, Vector + OpenSearch로 로그 중앙화를 마스터했습니다. 이제 Day 7에서는 Kubernetes의 꽃이라 할 수 있는 분산 스토리지 Ceph를 직접 구축하고 세 가지 스토리지 타입을 모두 테스트했습니다.
오늘 배운 것:
1. Ceph 아키텍처 완벽 이해 (RADOS, CRUSH, PG 개념)
2. Rook Operator로 단일 노드 Ceph 클러스터 구축
3. RBD 블록 스토리지 (ReadWriteOnce) - 동적 프로비저닝
4. CephFS 파일 시스템 (ReadWriteMany) - 다중 Pod 동시 접근
5. RGW 오브젝트 스토리지 (S3 API) - 사용자 및 버킷 생성
6. Ceph Dashboard 웹 UI로 클러스터 모니터링
7. 단일 노드 환경의 한계와 해결 방법
🤔 내가 이해한 것:
기존에는 hostPath나 emptyDir로 Pod에 볼륨을 붙였습니다. 하지만 이건 문제가 있었어요:
문제점:
❌ hostPath: 특정 노드에 종속 (Pod가 다른 노드로 옮겨가면 데이터 유실)
❌ emptyDir: Pod 삭제 시 데이터 사라짐
❌ NFS: 단일 장애점 (NFS 서버 다운 = 전체 다운)
Ceph의 해결책:
✅ 분산 스토리지: 데이터를 여러 노드에 자동 복제
✅ 자가 복구: 노드 장애 시 자동으로 다른 노드에 데이터 재배치
✅ 무제한 확장: 노드 추가 = 스토리지 용량 증가
✅ 3가지 인터페이스: Block (RBD), File (CephFS), Object (S3)
HostPath vs Ceph 비교:
HostPath 방식:
Pod A (cpu1 노드)
↓
/data/app (cpu1의 로컬 디스크)
→ Pod가 cpu2로 옮겨가면? 데이터 접근 불가!
Ceph 방식:
Pod A (어느 노드든)
↓
Ceph 클러스터 (cpu1 + cpu2 + gpu1)
├─ 복제본 1: cpu1
├─ 복제본 2: cpu2
└─ 복제본 3: gpu1
→ Pod가 어디로 옮겨가든 데이터 접근 가능!
→ 노드 1개 다운되어도 괜찮음!
RADOS (Reliable Autonomic Distributed Object Store):
CRUSH (Controlled Replication Under Scalable Hashing):
마법의 알고리즘!
- 중앙 조회 테이블 없음
- 클라이언트가 직접 계산해서 데이터 위치 찾음
- 노드 추가/제거 시 최소한의 데이터 이동
작동 원리:
Object Name + Cluster Map → Hash 계산
→ Placement Group (PG) 결정
→ CRUSH Rules 적용
→ 최종 OSD 위치 계산
Placement Groups (PG):
왜 필요한가?
100만 객체를 12 OSD에 직접 매핑하면?
→ 관리 복잡도: O(n×m) = 1,200만 관계
100만 객체 → 512 PG → 12 OSD로 매핑하면?
→ 관리 복잡도: O(n+m) = 100만 + 512 + 12
PG는 객체와 OSD 사이의 중간 레이어!
원래 계획: 3노드 HA 구성
cpu1 + cpu2 + gpu1 = 3노드 Ceph 클러스터
각 노드에 sdb 디스크 할당
복제본 3개 (size: 3)
변경된 계획: 단일 노드 학습용
이유:
# gpu1 노드 디스크 확인
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 465.8G 0 disk
├─sda1 8:1 0 465.7G 0 part /
└─sda2 8:2 0 1M 0 part
sdb 8:16 0 465.8G 0 disk ← 이걸 Ceph에 사용!
🎯 단일 노드 Ceph 설정의 핵심:
failureDomain: osd # host가 아닌 osd 단위
replicated:
size: 1 # 복제본 1개
requireSafeReplicaSize: false # 안전성 검사 비활성화
Rook이 뭐지?
# 1. CRD 설치
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/crds.yaml
customresourcedefinition.apiextensions.k8s.io/cephblockpools.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephfilesystems.ceph.rook.io created
customresourcedefinition.apiextensions.k8s.io/cephobjectstores.ceph.rook.io created
...
# 2. Common 리소스 (RBAC, ServiceAccount 등)
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/common.yaml
namespace/rook-ceph created
serviceaccount/rook-ceph-system created
...
# 3. Operator 배포
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/operator.yaml
deployment.apps/rook-ceph-operator created
# 4. Operator Pod 확인
$ kubectl get pods -n rook-ceph
NAME READY STATUS RESTARTS AGE
rook-ceph-operator-5b7c8d8d4-xxxxx 1/1 Running 0 2m
🎉 성공! Rook Operator가 실행되고 있습니다!
Rook Operator를 설치했으니, 이제 어떤 CRD가 제공되고 각 Pod가 무슨 역할을 하는지 정확히 이해해야 합니다!
$ kubectl get crd | grep ceph.rook.io
핵심 CRD 목록:
| CRD 이름 | 용도 | 예시 |
|---|---|---|
| CephCluster | Ceph 클러스터 전체 정의 | MON, MGR, OSD 개수, 버전 설정 |
| CephBlockPool | RBD용 Pool 생성 | replicapool (size=1, failureDomain=osd) |
| CephFilesystem | CephFS 파일시스템 | myfs (MDS 서버 포함) |
| CephObjectStore | RGW Object Storage | my-store (S3 API 엔드포인트) |
| CephObjectStoreUser | S3 사용자 생성 | my-s3-user (AccessKey/SecretKey) |
| CephClient | 외부 클라이언트 인증 | 다른 클러스터에서 접근 |
| CephRBDMirror | RBD 미러링 (재해복구) | 다른 클러스터로 복제 |
| CephNFS | NFS Gateway | NFS 프로토콜로 접근 |
🤔 왜 CRD가 필요한가?
전통적인 Ceph 구축:
# 수동 명령어 실행 (어렵고 복잡)
ceph-deploy mon create node1 node2 node3
ceph-deploy osd prepare node1:/dev/sdb
ceph osd pool create mypool 128
ceph fs new myfs myfs-metadata myfs-data
Rook + CRD 방식:
# 선언적 YAML (쉽고 재현 가능!)
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
name: rook-ceph
spec:
mon:
count: 3
storage:
nodes:
- name: "gpu1"
devices:
- name: "sdb"
→ kubectl apply 한 번으로 끝!
→ Operator가 알아서 MON, MGR, OSD 배포
→ Git으로 버전 관리 가능 (GitOps)
$ kubectl get pods -n rook-ceph
NAME READY STATUS
rook-ceph-operator-xxxxx 1/1 Running # Operator
rook-ceph-mon-a-xxxxx 1/1 Running # Monitor
rook-ceph-mgr-a-xxxxx 1/1 Running # Manager
rook-ceph-osd-0-xxxxx 1/1 Running # OSD
rook-ceph-mds-myfs-a-xxxxx 1/1 Running # MDS (CephFS용)
rook-ceph-mds-myfs-b-xxxxx 1/1 Running # MDS Standby
rook-ceph-rgw-my-store-a-xxxxx 1/1 Running # RGW (S3용)
rook-ceph-crashcollector-gpu1-xxxxx 1/1 Running # Crash 수집
rook-ceph-exporter-gpu1-xxxxx 1/1 Running # Prometheus 메트릭
csi-rbdplugin-xxxxx 2/2 Running # RBD CSI Driver
csi-rbdplugin-provisioner-xxxxx 5/5 Running # RBD Provisioner
csi-cephfsplugin-xxxxx 2/2 Running # CephFS CSI Driver
csi-cephfsplugin-provisioner-xxxxx 5/5 Running # CephFS Provisioner
rook-ceph-tools-xxxxx 1/1 Running # CLI 도구
역할:
- CephCluster CRD를 감시
- YAML 변경 시 실제 Ceph 리소스 생성/업데이트
- Pod 장애 시 자동 복구
- Kubernetes API와 Ceph 명령어를 연결하는 브릿지
예시:
CephBlockPool YAML 적용 → Operator가 감지
→ ceph osd pool create 명령 실행
→ Pool 생성 완료
역할:
- Cluster Map 유지 (MON, OSD, PG 상태)
- Quorum 형성 (과반수 합의)
- 클라이언트에게 Cluster Map 제공
왜 홀수 개?
- 2개: 1개 다운 시 Quorum 불가 (50% < 과반수)
- 3개: 1개 다운 OK (2/3 = 66% > 과반수)
- 1개: 단일 노드 학습용 (HA 불가)
통신 포트:
- 6789: Ceph v1 protocol
- 3300: Ceph v2 protocol
역할:
- Ceph Dashboard 웹 UI 제공
- Prometheus 메트릭 노출
- PG Autoscaler 실행
- REST API 제공
모듈:
- dashboard: 웹 UI (port 7000)
- prometheus: 메트릭 수집 (port 9283)
- pg_autoscaler: PG 수 자동 조정
- balancer: OSD 간 데이터 균형
Active-Standby:
- mgr-a: Active (요청 처리)
- mgr-b: Standby (장애 대기)
역할:
- 실제 데이터 저장 (sdb 디스크 사용)
- 데이터 복제 (다른 OSD로)
- Heartbeat (다른 OSD와 상태 확인)
- Backfilling (데이터 재배치)
OSD 번호:
- OSD.0: gpu1의 sdb 디스크
- (추가 노드 시 OSD.1, OSD.2, ...)
데이터 경로:
/var/lib/rook/osd0/
├─ block → /dev/sdb (심볼릭 링크)
├─ block.db (메타데이터, SSD 권장)
└─ block.wal (WAL, SSD 권장)
포트:
- 6800-7300: OSD 간 통신
역할:
- CephFS 파일 메타데이터 관리
- 디렉토리 구조, 권한, 소유자 정보
- inode 캐싱 (성능 향상)
왜 2개?
- mds-a: Active (실제 요청 처리)
- mds-b: Standby (장애 대기)
- Active 장애 시 Standby가 즉시 승격 (30초 이내)
메타데이터 vs 데이터:
메타데이터 (MDS가 관리):
- 파일명: /data/myfile.txt
- 크기: 1GB
- 권한: 0644
실제 데이터 (OSD에 저장):
- Object 1: myfile.txt.00000000 (4MB)
- Object 2: myfile.txt.00000001 (4MB)
- ...
역할:
- S3 호환 REST API 제공
- 버킷 및 오브젝트 관리
- 사용자 인증 (AccessKey/SecretKey)
- Multi-part Upload 지원
엔드포인트:
- http://rook-ceph-rgw-my-store.rook-ceph:80
- S3 API: PUT /bucket/object
데이터 흐름:
1. AWS CLI: aws s3 cp file.txt s3://mybucket/
2. RGW: S3 요청 → Ceph Object 변환
3. RADOS: Object를 PG로 매핑 → OSD 저장
역할:
- Kubernetes CSI 인터페이스 구현
- PVC 생성 시 실제 볼륨 Attach/Mount
- 각 노드마다 DaemonSet으로 배포
RBD CSI Driver:
- RBD 이미지 생성: rbd create
- 노드에 Map: rbd map /dev/rbd0
- 파일시스템 생성: mkfs.ext4
- Pod에 Mount: mount /dev/rbd0 /var/lib/kubelet/pods/...
CephFS CSI Driver:
- CephFS 서브볼륨 생성
- Ceph Client 마운트
- FUSE 또는 Kernel Driver 사용
역할:
- PVC 생성 요청 감시
- 동적 프로비저닝 실행
- PV 생성 및 PVC 바인딩
워크플로우:
1. User: kubectl apply -f pvc.yaml
2. Provisioner: PVC 감지 (StorageClass 확인)
3. Provisioner: Ceph에 볼륨 생성 (RBD 또는 CephFS)
4. Provisioner: PV 오브젝트 생성
5. Kubernetes: PVC ↔ PV 바인딩 (Bound 상태)
왜 5개 컨테이너?
- csi-provisioner: 메인 프로비저너
- csi-resizer: 볼륨 크기 조정
- csi-snapshotter: 스냅샷 생성
- csi-attacher: 볼륨 Attach
- liveness: Health Check
역할:
- Ceph 데몬 Crash 시 덤프 수집
- /var/lib/rook/crash/ 디렉토리 모니터링
- Crash 정보를 MON에 전송
- 문제 디버깅에 사용
확인:
$ ceph crash ls
$ ceph crash info <crash-id>
역할:
- Ceph 메트릭을 Prometheus 포맷으로 노출
- OSD 상태, Pool 사용량, PG 상태 등
- Day 6 Prometheus와 연동
메트릭 예시:
- ceph_health_status (0=OK, 1=WARN, 2=ERR)
- ceph_osd_up (OSD UP 개수)
- ceph_pool_stored_bytes (Pool 사용량)
ServiceMonitor 연동:
monitoring:
enabled: true
→ MGR이 ServiceMonitor 생성
→ Prometheus가 자동 수집
역할:
- ceph 명령어 실행 환경
- 디버깅 및 관리 작업
사용법:
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph osd tree
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- rados df
포함된 도구:
- ceph: 메인 CLI
- rbd: RBD 관리
- rados: Object 관리
- ceph-volume: OSD 관리
예시: MySQL Pod가 10GB RBD PVC 요청
1. kubectl apply -f mysql-pvc.yaml
↓
2. csi-rbdplugin-provisioner가 감지
↓
3. Provisioner → Ceph: rbd create replicapool/pvc-xxxxx --size 10240
↓
4. CRUSH 알고리즘: PG.1 계산
↓
5. PG.1 → OSD.0 매핑 (단일 노드이므로 OSD.0만)
↓
6. OSD.0: /dev/sdb에 데이터 저장
↓
7. Provisioner: PV 오브젝트 생성
↓
8. Kubernetes: PVC ↔ PV 바인딩 (Bound)
↓
9. MySQL Pod 스케줄링 → cpu1 노드
↓
10. csi-rbdplugin (cpu1): rbd map → /dev/rbd0
↓
11. csi-rbdplugin: mount /dev/rbd0 → /var/lib/kubelet/pods/.../volumes/
↓
12. MySQL 컨테이너: /var/lib/mysql 마운트 완료!
Multi-node 환경이라면?
PG.1 → [OSD.0, OSD.1, OSD.2] (3중 복제)
- Primary OSD.0 (cpu1): 쓰기 처리
- Replica OSD.1 (cpu2): 복제본 저장
- Replica OSD.2 (gpu1): 복제본 저장
→ 노드 1개 다운되어도 데이터 안전!
중요! OSD로 사용할 디스크는 완전히 깨끗해야 합니다!
# sdb 디스크 완전 초기화
$ wipefs -a /dev/sdb
$ sgdisk --zap-all /dev/sdb
$ dd if=/dev/zero of=/dev/sdb bs=1M count=100
# 확인 (FSTYPE이 비어있어야 함)
$ lsblk -f | grep sdb
sdb
단일 노드용 cluster YAML 작성:
# ~/ceph/ceph-cluster.yaml
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
name: rook-ceph
namespace: rook-ceph
spec:
cephVersion:
image: quay.io/ceph/ceph:v18.2.0
dataDirHostPath: /var/lib/rook
mon:
count: 1 # 단일 노드이므로 MON 1개
mgr:
count: 1
modules:
- name: pg_autoscaler
enabled: true
dashboard:
enabled: true
ssl: false
monitoring:
enabled: true # Prometheus 연동!
storage:
useAllNodes: false
useAllDevices: false
nodes:
- name: "gpu1"
devices:
- name: "sdb" # 465.8GB 디스크
disruptionManagement:
managePodBudgets: false # 단일 노드에서는 비활성화
적용:
$ kubectl apply -f ~/ceph/ceph-cluster.yaml
cephcluster.ceph.rook.io/rook-ceph created
# Pod 생성 확인 (30초~1분 소요)
$ kubectl get pods -n rook-ceph
NAME READY STATUS RESTARTS AGE
rook-ceph-mon-a-xxxxx 1/1 Running 0 45s
rook-ceph-mgr-a-xxxxx 1/1 Running 0 30s
rook-ceph-osd-prepare-gpu1-xxxxx 0/1 Completed 0 15s
rook-ceph-osd-0-xxxxx 1/1 Running 0 10s
🎉 MON, MGR, OSD 모두 Running!
Ceph 클러스터가 올라오긴 했지만, MGR Pod 로그를 보니 에러가 보였습니다!
문제 발견:
$ kubectl logs -n rook-ceph rook-ceph-mgr-a-xxxxx
Error: servicemonitors.monitoring.coreos.com "rook-ceph-mgr" is forbidden:
User "system:serviceaccount:rook-ceph:rook-ceph-system" cannot get resource "servicemonitors"
in API group "monitoring.coreos.com" at the cluster scope
상황 이해:
Ceph MGR의 역할:
monitoring.enabled: true 설정 시 ServiceMonitor CRD 자동 생성ServiceMonitor란?:
# Prometheus Operator의 CRD
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: rook-ceph-mgr
spec:
endpoints:
- port: http-metrics # Ceph MGR 메트릭 수집
권한 문제:
Ceph MGR Pod (rook-ceph-system ServiceAccount)
↓
"ServiceMonitor 생성하려고 함"
↓
Kubernetes API Server: "권한 없음! Forbidden!"
1. ServiceAccount (신원 증명):
$ kubectl get sa -n rook-ceph rook-ceph-system
NAME SECRETS AGE
rook-ceph-system 0 10m
serviceAccountName: rook-ceph-system 설정됨2. ClusterRole (권한 정의):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rook-ceph-servicemonitor-access
rules:
- apiGroups: ["monitoring.coreos.com"] # Prometheus Operator API
resources: ["servicemonitors"] # ServiceMonitor CRD
verbs: ["get", "list", "watch", "create", "update", "delete"] # CRUD 권한
3. ClusterRoleBinding (계정과 권한 연결):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rook-ceph-servicemonitor-access
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: rook-ceph-servicemonitor-access # 위에서 정의한 Role
subjects:
- kind: ServiceAccount
name: rook-ceph-system # 이 계정에게
namespace: rook-ceph # rook-ceph 네임스페이스의
이유:
Rook이 제공하는 기본 RBAC:
# common.yaml에 포함된 기본 권한
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
# ... (ServiceMonitor는 없음!)
RBAC 생성:
# ~/ceph/rook-servicemonitor-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rook-ceph-servicemonitor-access
rules:
- apiGroups: ["monitoring.coreos.com"]
resources: ["servicemonitors"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: rook-ceph-servicemonitor-access
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: rook-ceph-servicemonitor-access
subjects:
- kind: ServiceAccount
name: rook-ceph-system
namespace: rook-ceph
적용:
$ kubectl apply -f ~/ceph/rook-servicemonitor-rbac.yaml
clusterrole.rbac.authorization.k8s.io/rook-ceph-servicemonitor-access created
clusterrolebinding.rbac.authorization.k8s.io/rook-ceph-servicemonitor-access created
결과 확인:
# MGR이 ServiceMonitor 생성 성공!
$ kubectl get servicemonitor -n rook-ceph
NAME AGE
rook-ceph-mgr 2m
# Prometheus가 자동 수집 시작
$ kubectl get servicemonitor -n rook-ceph rook-ceph-mgr -o yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: rook-ceph-mgr
namespace: rook-ceph
spec:
endpoints:
- port: http-metrics # MGR의 9283 포트
interval: 30s
namespaceSelector:
matchNames:
- rook-ceph
selector:
matchLabels:
app: rook-ceph-mgr
Prometheus 확인:
# Prometheus UI (http://<NODE-IP>:30090) → Targets
# rook-ceph/rook-ceph-mgr/0 (http://10.x.x.x:9283/metrics) UP
✅ 해결! MGR이 정상적으로 ServiceMonitor를 생성하고, Prometheus가 Ceph 메트릭을 수집하기 시작했습니다!
RBAC의 중요성:
실무 팁:
# 권한 에러 디버깅
$ kubectl logs -n <namespace> <pod-name> | grep -i "forbidden\|unauthorized"
# ServiceAccount 권한 확인
$ kubectl auth can-i create servicemonitors \
--as=system:serviceaccount:rook-ceph:rook-ceph-system \
-n rook-ceph
no
# RBAC 적용 후
$ kubectl auth can-i create servicemonitors \
--as=system:serviceaccount:rook-ceph:rook-ceph-system \
-n rook-ceph
yes # ✅
# Toolbox 배포
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/toolbox.yaml
deployment.apps/rook-ceph-tools created
# Toolbox에 들어가서 Ceph 상태 확인
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
cluster:
id: 6fa3b356-4964-460e-9a29-e8e350febeff
health: HEALTH_WARN
OSD count 1 < osd_pool_default_size 3
services:
mon: 1 daemons, quorum a (age 5m)
mgr: a(active, since 4m)
osd: 1 osds: 1 up (since 3m), 1 in (since 3m)
data:
pools: 0 pools, 0 pgs
objects: 0 objects, 0 B
usage: 27 MiB used, 466 GiB / 466 GiB avail
HEALTH_WARN는 정상입니다!
RBD (RADOS Block Device):
트러블슈팅 2: PG undersized+peered 문제
첫 시도 (실패):
# GitHub의 기본 storageclass 적용
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/release-1.14/deploy/examples/csi/rbd/storageclass.yaml
# PVC 생성
$ kubectl apply -f test-pvc.yaml
# PVC 상태 확인
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-pvc Pending rook-ceph-block 2m
문제 확인:
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
data:
pools: 1 pools, 32 pgs
pgs: 32 undersized+peered ← 문제!
100.000% pgs not active
원인: 기본 BlockPool 설정이 다중 노드용
failureDomain: host # ❌ 여러 호스트에 분산
replicated:
size: 3 # ❌ 복제본 3개 필요
해결: 단일 노드용 BlockPool 생성
# ~/ceph/single-node-blockpool.yaml
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
name: replicapool
namespace: rook-ceph
spec:
failureDomain: osd # ✅ OSD 단위로 변경
replicated:
size: 1 # ✅ 복제본 1개
requireSafeReplicaSize: false # ✅ 안전성 검사 비활성화
# 기존 pool 삭제 후 재생성
$ kubectl delete cephblockpool -n rook-ceph replicapool
$ kubectl apply -f ~/ceph/single-node-blockpool.yaml
# PG 상태 확인
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
data:
pools: 1 pools, 32 pgs
pgs: 32 active+clean ← ✅ 성공!
# ~/ceph/test-ceph-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-ceph-pvc
spec:
accessModes:
- ReadWriteOnce # RWO
storageClassName: rook-ceph-block
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: test-ceph-pod
spec:
containers:
- name: test
image: busybox:1.28
command: ["sh", "-c", "echo 'Hello Ceph!' > /data/test.txt && cat /data/test.txt && sleep 3600"]
volumeMounts:
- name: ceph-volume
mountPath: /data
volumes:
- name: ceph-volume
persistentVolumeClaim:
claimName: test-ceph-pvc
$ kubectl apply -f ~/ceph/test-ceph-pvc.yaml
persistentvolumeclaim/test-ceph-pvc created
pod/test-ceph-pod created
# PVC 상태 확인
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
test-ceph-pvc Bound pvc-4d7204b9-98c0-416b-9574-25af51682854 1Gi RWO
# Pod 상태 확인
$ kubectl get pod test-ceph-pod
NAME READY STATUS RESTARTS AGE
test-ceph-pod 1/1 Running 0 20s
# 로그 확인
$ kubectl logs test-ceph-pod
Hello Ceph!
🎉 RBD 블록 스토리지 성공!
ReadWriteMany (RWX)의 의미:
사용 사례:
CephFS는 MDS (Metadata Server)가 필요합니다!
# ~/ceph/cephfs-filesystem.yaml
apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
name: myfs
namespace: rook-ceph
spec:
metadataPool:
failureDomain: osd
replicated:
size: 1
requireSafeReplicaSize: false
dataPools:
- name: data0
failureDomain: osd
replicated:
size: 1
requireSafeReplicaSize: false
metadataServer:
activeCount: 1
activeStandby: false
$ kubectl apply -f ~/ceph/cephfs-filesystem.yaml
cephfilesystem.ceph.rook.io/myfs created
# MDS Pod 확인
$ kubectl get pods -n rook-ceph | grep mds
rook-ceph-mds-myfs-a-xxxxx 1/1 Running 0 30s
rook-ceph-mds-myfs-b-xxxxx 1/1 Running 0 26s
# CephFS 상태 확인
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph fs status myfs
myfs - 0 clients
====
RANK STATE MDS ACTIVITY DNS INOS DIRS CAPS
0 active myfs-a Reqs: 0 /s 12 15 14 0
POOL TYPE USED AVAIL
myfs-metadata metadata 40.0k 442G
myfs-data0 data 0 442G
STANDBY MDS
myfs-b
✅ Active MDS 1개, Standby MDS 1개 정상 작동!
2개 Pod가 동시에 같은 볼륨 사용:
# ~/ceph/cephfs-storageclass.yaml
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-data0
# ... CSI 시크릿 설정 ...
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-cephfs-pvc
spec:
accessModes:
- ReadWriteMany # ✅ RWX!
storageClassName: rook-cephfs
resources:
requests:
storage: 1Gi
---
# Pod 1
apiVersion: v1
kind: Pod
metadata:
name: test-cephfs-pod1
spec:
containers:
- name: test
image: busybox:1.28
command: ["sh", "-c", "while true; do echo '[Pod 1] Writing...' >> /data/pod1.txt; ls -lh /data; sleep 10; done"]
volumeMounts:
- name: cephfs-volume
mountPath: /data
volumes:
- name: cephfs-volume
persistentVolumeClaim:
claimName: test-cephfs-pvc
---
# Pod 2 (같은 PVC 사용!)
apiVersion: v1
kind: Pod
metadata:
name: test-cephfs-pod2
spec:
containers:
- name: test
image: busybox:1.28
command: ["sh", "-c", "while true; do echo '[Pod 2] Writing...' >> /data/pod2.txt; ls -lh /data; sleep 10; done"]
volumeMounts:
- name: cephfs-volume
mountPath: /data
volumes:
- name: cephfs-volume
persistentVolumeClaim:
claimName: test-cephfs-pvc # ✅ 같은 PVC!
$ kubectl apply -f ~/ceph/cephfs-storageclass.yaml
storageclass.storage.k8s.io/rook-cephfs created
persistentvolumeclaim/test-cephfs-pvc created
pod/test-cephfs-pod1 created
pod/test-cephfs-pod2 created
# PVC 상태 (RWX 확인!)
$ kubectl get pvc test-cephfs-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
test-cephfs-pvc Bound pvc-b267c8ad-ebae-4253-8685-e5d6487cef5e 1Gi RWX
# 두 Pod 모두 Running
$ kubectl get pods test-cephfs-pod1 test-cephfs-pod2
NAME READY STATUS RESTARTS AGE
test-cephfs-pod1 1/1 Running 0 1m
test-cephfs-pod2 1/1 Running 0 1m
# Pod 1에서 파일 목록 확인
$ kubectl exec test-cephfs-pod1 -- ls -lh /data
total 3
-rw-r--r-- 1 root root 1.2K Nov 6 15:11 pod1.txt
-rw-r--r-- 1 root root 1.2K Nov 6 15:11 pod2.txt
# Pod 2에서도 동일하게 보임!
$ kubectl exec test-cephfs-pod2 -- ls -lh /data
total 3
-rw-r--r-- 1 root root 1.2K Nov 6 15:11 pod1.txt
-rw-r--r-- 1 root root 1.2K Nov 6 15:11 pod2.txt
# Pod 2에서 Pod 1의 파일 읽기
$ kubectl exec test-cephfs-pod2 -- head -5 /data/pod1.txt
[Pod 1] Writing...
[Pod 1] Writing...
[Pod 1] Writing...
🎉 진정한 공유 파일 시스템! 두 Pod가 같은 데이터를 실시간으로 공유합니다!
S3 호환 API:
# ~/ceph/ceph-objectstore.yaml
apiVersion: ceph.rook.io/v1
kind: CephObjectStore
metadata:
name: my-store
namespace: rook-ceph
spec:
metadataPool:
failureDomain: osd
replicated:
size: 1
requireSafeReplicaSize: false
dataPool:
failureDomain: osd
replicated:
size: 1
requireSafeReplicaSize: false
gateway:
port: 80
instances: 1
$ kubectl apply -f ~/ceph/ceph-objectstore.yaml
cephobjectstore.ceph.rook.io/my-store created
# RGW Pod 확인
$ kubectl get pods -n rook-ceph -l app=rook-ceph-rgw
NAME READY STATUS RESTARTS AGE
rook-ceph-rgw-my-store-a-xxxxx 1/1 Running 0 1m
# RGW 서비스 확인
$ kubectl get svc -n rook-ceph | grep rgw
rook-ceph-rgw-my-store ClusterIP 10.107.184.210 <none> 80/TCP 1m
# ~/ceph/s3-user.yaml
apiVersion: ceph.rook.io/v1
kind: CephObjectStoreUser
metadata:
name: my-s3-user
namespace: rook-ceph
spec:
store: my-store
displayName: "My S3 User"
$ kubectl apply -f ~/ceph/s3-user.yaml
cephobjectstoreuser.ceph.rook.io/my-s3-user created
# Access Key와 Secret Key 확인
$ kubectl get secret -n rook-ceph rook-ceph-object-user-my-store-my-s3-user -o jsonpath='{.data.AccessKey}' | base64 -d
VZ4O1JVZQZX92XDKCNPM
$ kubectl get secret -n rook-ceph rook-ceph-object-user-my-store-my-s3-user -o jsonpath='{.data.SecretKey}' | base64 -d
VbtubT0XNLDjrzHcKoVKeNYvXjdEhfFOT2la2GVE
✅ S3 API 사용 준비 완료! 이제 AWS CLI나 boto3로 접근 가능합니다.
트러블슈팅 3: NodePort가 계속 ClusterIP로 되돌아가는 문제
# Dashboard 서비스 확인
$ kubectl get svc -n rook-ceph rook-ceph-mgr-dashboard
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
rook-ceph-mgr-dashboard ClusterIP 10.104.241.243 <none> 7000/TCP
# NodePort로 변경 시도
$ kubectl patch svc rook-ceph-mgr-dashboard -n rook-ceph -p '{"spec":{"type":"NodePort"}}'
service/rook-ceph-mgr-dashboard patched
# 잠시 후 다시 확인하면...
$ kubectl get svc -n rook-ceph rook-ceph-mgr-dashboard
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
rook-ceph-mgr-dashboard ClusterIP 10.104.241.243 <none> 7000/TCP ← 다시 ClusterIP!
원인: Rook Operator가 서비스를 관리하면서 원래 상태로 복원
해결: 별도의 NodePort 서비스 생성
# ~/ceph/dashboard-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: ceph-dashboard-external
namespace: rook-ceph
spec:
type: NodePort
selector:
app: rook-ceph-mgr # MGR Pod를 타겟으로
ports:
- name: dashboard
port: 7000
targetPort: 7000
protocol: TCP
$ kubectl apply -f ~/ceph/dashboard-nodeport.yaml
service/ceph-dashboard-external created
$ kubectl get svc -n rook-ceph ceph-dashboard-external
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
ceph-dashboard-external NodePort 10.110.34.58 <none> 7000:31383/TCP
# 접근 테스트
$ curl -I http://172.30.1.38:31383
HTTP/1.1 200 OK
Server: Ceph-Dashboard
✅ 성공! Dashboard에 접근 가능합니다!
접속 정보:
URL: http://172.30.1.38:31383
Username: admin
Password: (Secret에서 확인)
# Password 확인
$ kubectl get secret -n rook-ceph rook-ceph-dashboard-password -o jsonpath='{.data.password}' | base64 -d
\nLp%F2g(aDx<PzY.$60
Dashboard 메뉴:
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph status
cluster:
id: 6fa3b356-4964-460e-9a29-e8e350febeff
health: HEALTH_WARN
11 pool(s) have no replicas configured
OSD count 1 < osd_pool_default_size 3
services:
mon: 1 daemons, quorum a (age 1h)
mgr: a(active, since 1h)
mds: 1/1 daemons up, 1 standby ← CephFS
osd: 1 osds: 1 up, 1 in
rgw: 1 daemon active ← RGW
data:
volumes: 1/1 healthy ← CephFS
pools: 11 pools, 168 pgs
objects: 252 objects, 5.7 MiB
usage: 33 MiB used, 466 GiB / 466 GiB avail
pgs: 168 active+clean ← ✅ 모든 PG 정상!
스토리지 사용량:
$ kubectl exec -n rook-ceph deploy/rook-ceph-tools -- ceph df
--- POOLS ---
POOL ID PGS STORED OBJECTS USED %USED MAX AVAIL
replicapool 2 32 672 KiB 13 680 KiB 0 442 GiB ← RBD
myfs-metadata 3 16 455 KiB 21 484 KiB 0 442 GiB ← CephFS
myfs-data0 4 32 22 KiB 3 28 KiB 0 442 GiB ← CephFS
my-store.rgw.* ... ... (8개 Pool) ← RGW
Kubernetes 리소스:
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
test-ceph-pvc Bound pvc-4d7204b9-98c0-416b-9574-25af51682854 1Gi RWO
test-cephfs-pvc Bound pvc-b267c8ad-ebae-4253-8685-e5d6487cef5e 1Gi RWX
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
test-ceph-pod 1/1 Running 0 30m
test-cephfs-pod1 1/1 Running 0 23m
test-cephfs-pod2 1/1 Running 0 23m
CRUSH 알고리즘의 아름다움:
Placement Group의 필요성:
단일 노드 Ceph의 한계:
문제점:
❌ SPOF (Single Point of Failure)
❌ 네트워크 분산 이점 없음
❌ 복제 효과 없음 (같은 서버)
❌ 장애 복구 불가
결론: 단일 노드는 학습용!
프로덕션은 최소 3노드 이상!
프로덕션 권장 구성:
최소:
노드: 3개
OSD/노드: 4개 (총 12 OSD)
복제본: 3
네트워크: 10GbE
이상적:
노드: 5개+
OSD/노드: 10개+
복제본: 3
네트워크: 25GbE+
전용 Storage 노드 분리
| 타입 | Access Mode | 용도 | 기술 |
|---|---|---|---|
| RBD | RWO | 단일 Pod 전용 블록 | RADOS Block Device |
| CephFS | RWX | 다중 Pod 공유 파일시스템 | MDS + POSIX |
| RGW | - | S3 API 오브젝트 스토리지 | RADOS Gateway |
사용 예시:
선언적 관리:
CephCluster CRD 작성
↓
kubectl apply
↓
Operator가 자동으로:
- MON/MGR/OSD Pod 생성
- Service, ConfigMap 생성
- PG 계산 및 최적화
- 장애 복구
vs 수동 Ceph 배포:
ceph-deploy 명령어 수십 개증상: PVC가 Pending 상태로 멈춤
원인: 기본 BlockPool 설정이 3복제본 요구
교훈:
ceph status로 PG 상태 먼저 확인!증상: MGR Pod 로그에 권한 에러
원인: Prometheus Operator CRD 접근 권한 없음
교훈:
증상: NodePort로 변경해도 계속 ClusterIP로 되돌아감
원인: Rook Operator가 서비스 관리
교훈:
증상: OSD 생성 실패
원인: 파티션 테이블 잔여
교훈:
# 완전 초기화 3종 세트
wipefs -a /dev/sdb
sgdisk --zap-all /dev/sdb
dd if=/dev/zero of=/dev/sdb bs=1M count=100
Day 7에서 Ceph 분산 스토리지의 기초를 다졌습니다. 이제 Day 8에서는 프로덕션 환경을 위한 고급 주제를 다룰 예정입니다:
Ceph는 복잡하지만 강력합니다. 단일 노드로 학습했지만, 프로덕션에서는 반드시 멀티 노드로 구성해야 합니다.
핵심 요약:
Day 8에서 만나요! 🚀