1편 에서 솔로몬을 storage tier 로 정했습니다. 이번 글은 그 솔로몬에 PostgreSQL 8 개를 StatefulSet 으로 박는 과정 을 정리합니다. local-path-provisioner 의 nodePathMap, WaitForFirstConsumer, Retain 같은 키워드가 핵심입니다.

이 글에서 다루는 것

  • 왜 NFS 가 아니라 local-path 를 골랐는가
  • StorageClass 3 가지 옵션의 의미 (volumeBindingMode, reclaimPolicy, nodePathMap)
  • solomon-local StorageClass 정의
  • StatefulSet → PVC → PV 가 어떻게 솔로몬으로 빨려 들어가는지
  • 8 개 DB 를 일괄 마이그한 결과

1. 왜 local-path 인가 — NFS 와 비교

항목 local-path (선택) NFS
성능 (DB IOPS) ★★★★★ (로컬 SSD) ★★ (네트워크 RTT)
노드 이동 가능 ❌ (노드 종속) ✅ (어느 노드에서나)
운영 복잡도 ★ (K3s 기본 내장) ★★★ (NFS 서버 별도)
동시 마운트 (RWX) ❌ (RWO 만)

DB 는 IOPS 가 우선 이고, RWO 면 충분합니다. 노드 이동 못 하는 건 단점이지만 어차피 솔로몬에만 박을 거라 무관. → local-path 선택.

(참고로 MinIO 같은 RWX 가 필요한 워크로드는 솔로몬에 NFS 서버 하나 띄워서 두 가지를 병용합니다 — 이건 다른 글로.)


2. local-path-provisioner 가 PV 를 어디에 만드는지 강제하기

K3s 에 기본 내장된 local-path-provisioner 는 그대로 두면 각 노드의 /var/lib/rancher/k3s/storage/ 에 PV 를 만듭니다. 솔로몬에만 박으려면 nodePathMap 을 ConfigMap 으로 덮어씌워야 합니다.

# kube-system/local-path-config
apiVersion: v1
kind: ConfigMap
metadata:
  name: local-path-config
  namespace: kube-system
data:
  config.json: |
    {
      "nodePathMap": [
        {
          "node": "solomon",
          "paths": ["/opt/local-path-provisioner/solomon"]
        },
        {
          "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
          "paths": []   ← ★ 빈 배열 = 다른 노드에는 PV 못 만듦
        }
      ]
    }

paths: [] 가 핵심입니다. 이걸 안 하면 다른 노드에서 Pod 가 뜰 때 거기에 PV 가 만들어져버려서 다음에 같은 PVC 가 다른 노드로 가면 데이터를 못 봅니다.

솔로몬 콘솔 사전 작업:

sudo mkdir -p /opt/local-path-provisioner/solomon
sudo chmod 755 /opt/local-path-provisioner/solomon

3. StorageClass solomon-local 정의

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: solomon-local
  annotations:
    storageclass.kubernetes.io/is-default-class: "false"
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer   # ★
reclaimPolicy: Retain                      # ★
parameters:
  nodePath: /opt/local-path-provisioner/solomon

옵션 3 개의 의미

volumeBindingMode: WaitForFirstConsumer PVC 만들었다고 PV 바로 안 만듦. Pod 가 스케줄링될 때까지 대기 했다가 그 노드에 PV 를 만듭니다. → Pod 와 PV 의 노드 어피니티가 자동으로 맞춰짐. 즉 “Pod 가 솔로몬에 떠야 → 솔로몬에 PV 가 생김” 이라는 흐름이 생김.

reclaimPolicy: Retain PVC 를 지워도 PV 와 디스크 데이터는 안 지워짐. 실수 방어 용. (default 인 Delete 였으면 kubectl delete pvc 한 번에 데이터 날라감.)

nodePath local-path-provisioner 가 디렉토리를 만들 base 경로. ConfigMap 에 적은 값과 일치해야 함.


4. StatefulSet 에서 솔로몬으로 강제

PVC 만으로는 다른 워커에 Pod 가 떠버릴 수 있습니다. StatefulSet 의 template.spec 에 nodeSelector + tolerations 를 추가해야 합니다:

apiVersion: apps/v1
kind: StatefulSet
metadata: { name: jen-postgres }
spec:
  serviceName: jen-postgres
  replicas: 1
  template:
    spec:
      nodeSelector:
        kubernetes.io/hostname: solomon   # ★ Pod 솔로몬 강제
      tolerations:
        - key: storage-only               # ★ 솔로몬 taint 통과
          operator: Exists
          effect: NoSchedule
      priorityClassName: lemuel-critical  # ★ OOM 시 살아남기
      containers:
        - name: postgres
          image: postgres:16-alpine
          # ...
  volumeClaimTemplates:
    - metadata: { name: data }
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: solomon-local
        resources:
          requests:
            storage: 50Gi

흐름 정리

[StatefulSet] → Pod 생성 요청
       ↓
[Scheduler] nodeSelector=solomon → 솔로몬 선택
       ↓
[StatefulSet controller] PVC 생성 (jen-postgres-data-jen-postgres-0)
       ↓
[local-path-provisioner] WaitForFirstConsumer
   → Pod 가 솔로몬에 스케줄됨 확인
   → /opt/local-path-provisioner/solomon/<uuid> 에 디렉토리 생성
   → PV 생성 + PVC 바인딩
       ↓
[Pod] 솔로몬 SSD 에 마운트, 시작

5. 8 개 DB 일괄 마이그 — 자동화 스크립트

기존 docker compose 로 떠 있던 PostgreSQL 8 개 (jen / dart / pilgrim / fashion / goods / cost / crypto / trading) 를 한 줄에 옮기는 스크립트:

#!/bin/bash
# migrate-pg-to-solomon.sh
NS=$1
DB=$2
OLD_HOST=$3
OLD_PORT=$4
PGUSER=$5
PGPASS=$6

# 1) docker compose 에서 dump
ssh $OLD_HOST "PGPASSWORD=$PGPASS pg_dump -h localhost -p $OLD_PORT \
  -U $PGUSER -d $DB --clean --if-exists" > /tmp/${DB}.sql

# 2) R2 로 백업
aws s3 cp /tmp/${DB}.sql s3://lemuel-backup/migration/${DB}-$(date +%Y%m%d).sql

# 3) K3s namespace + StatefulSet 생성 (helm 차트)
helm upgrade --install ${NS} ./charts/postgres-on-solomon \
  --namespace ${NS} --create-namespace \
  --set postgres.database=${DB}

# 4) Pod ready 대기
kubectl wait --for=condition=ready pod/${NS}-postgres-0 \
  -n ${NS} --timeout=300s

# 5) 복원
kubectl exec -n ${NS} ${NS}-postgres-0 -- \
  psql -U ${PGUSER} -d postgres -c "CREATE DATABASE ${DB};"
cat /tmp/${DB}.sql | kubectl exec -i -n ${NS} ${NS}-postgres-0 -- \
  psql -U ${PGUSER} -d ${DB}

# 6) 검증
kubectl exec -n ${NS} ${NS}-postgres-0 -- \
  psql -U ${PGUSER} -d ${DB} -c "\\dt+" | head

이걸 8 번 돌려서 약 30GB / 91만 row 를 옮겼습니다. 한 번에 끝나는 건 아니고 schema 가 public 이 아닌 케이스 (예: opslab) 같은 함정이 있어서 그건 건건이 잡았습니다.


6. 결과 — 솔로몬에서 본 디스크

$ ssh solomon df -h /opt/local-path-provisioner/solomon
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1       686G   28G  658G   4% /opt/local-path-provisioner/solomon

$ ls /opt/local-path-provisioner/solomon
pvc-a3f1...  pvc-b2c5...  pvc-c4d8...  ...  (8 개)
$ kubectl get pvc --all-namespaces \
    -o custom-columns=NS:.metadata.namespace,PVC:.metadata.name,SIZE:.spec.resources.requests.storage \
    | grep postgres
NS              PVC                                  SIZE
jen-prod        data-jen-postgres-0                  50Gi
dart-prod       data-dart-postgres-0                 30Gi
pilgrim-prod    data-pilgrim-postgres-0              30Gi
fashion-prod    data-fashion-postgres-0              30Gi
goods-prod      data-goodsonline-postgres-0          50Gi
cost-prod       data-cost-postgres-0                 20Gi
crypto-prod     data-crypto-postgres-0               20Gi
trading-prod    data-trading-postgres-0              20Gi

250Gi 확보, 28Gi 사용. 솔로몬 686GB 의 4% 만 썼고, 600GB 가 남아있어서 ELN, Settlement 같은 큰 거 들어와도 여유 있음.


7. 함정 — 내가 빠진 것

  1. nodePathMap.DEFAULT_PATH_FOR_NON_LISTED_NODES.paths: [] 빠뜨려서 솔로몬 아닌 다른 노드에서 PVC 가 떠버린 적 있음. → ConfigMap 다시 적용 + 잘못 만들어진 PV 수동 삭제
  2. opslab schema 못 보고 \\dt 결과가 0 으로 나옴. \\dn 으로 schema 목록 먼저 확인해야 함
  3. reclaimPolicy: Delete 였던 시절 PVC 잘못 지워서 data 통째로 날림 → 백업이 살려줌. 이후 모든 솔로몬 SC 는 Retain 필수

다음 글

  • K3s 실전 3편 — LimitRange / ResourceQuota / PriorityClass 로 OOM 방어
  • DB 가 솔로몬에 들어왔으니, 이제 OOM 시 DB 가 가장 마지막에 죽도록 우선순위를 매겨줘야 합니다.