refactor: organize infra/ into compute/network/security/data/platform
This commit is contained in:
178
infra/data/backup.md
Normal file
178
infra/data/backup.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
title: 백업 파이프라인
|
||||
updated: 2026-04-15
|
||||
tags: [infra, backup]
|
||||
---
|
||||
|
||||
## Longhorn PVC 백업 (K3s)
|
||||
|
||||
BackupTarget `default` → R2 버킷 `longhorn-backup` (시크릿 `longhorn-backup-r2`). RecurringJob 4종 (critical-snapshot 매시 UTC, critical-backup 6h UTC, standard-snapshot `0 18 * * *` UTC = KST 03:00, standard-backup `0 19 * * *` UTC = KST 04:00).
|
||||
|
||||
### RecurringJob 그룹 볼륨 라벨 — **정확한 키 주의**
|
||||
|
||||
Longhorn 컨트롤러가 인식하는 라벨 키는 **`recurring-job-group.longhorn.io/<group>=enabled`** (대시 포함). `recurringjob-group.longhorn.io/...` (대시 누락) 은 무효이며 컨트롤러가 볼륨을 찾지 못한다.
|
||||
|
||||
```bash
|
||||
# 신규 볼륨을 critical 그룹에 등록
|
||||
kubectl -n longhorn-system label volumes.longhorn.io <pvc-...> \
|
||||
recurring-job-group.longhorn.io/critical=enabled --overwrite
|
||||
|
||||
# 라벨 검증 (선택자가 볼륨을 실제로 잡는지)
|
||||
kubectl -n longhorn-system get volumes.longhorn.io \
|
||||
-l recurring-job-group.longhorn.io/critical=enabled
|
||||
```
|
||||
|
||||
라벨 기록이 꼬였을 때의 대응은 [[2026-04-15-longhorn-backup-label-typo|history]] 참고.
|
||||
|
||||
## incus 백업 (inbest 데이터)
|
||||
|
||||
3단계 백업 파이프라인. 소스: incus-kr2 inbest 프로젝트 `inbest-data` 커스텀 볼륨.
|
||||
|
||||
### 1. rsync → NAS
|
||||
|
||||
- **호스트**: incus-kr2
|
||||
- **스크립트**: `/usr/local/bin/backup-inbest.sh`
|
||||
- **systemd**: `backup-inbest.timer` / `backup-inbest.service`
|
||||
- **스케줄**: 매일 03:00 (±5분)
|
||||
- **소스**: `/var/lib/incus/storage-pools/default/custom/inbest_inbest-data/`
|
||||
- **대상**: `kaffa@192.168.205.100:/volume1/incus/inbest/`
|
||||
- **제외**: `*/data/session/`
|
||||
- **SSH 키**: `/home/kaffa/.ssh/id_ed25519`
|
||||
- **옵션**: `rsync -rlz --omit-dir-times --delete` (Synology 퍼미션 호환)
|
||||
|
||||
### 2. btrfs 스냅샷 (NAS)
|
||||
|
||||
- rsync 완료 직후 자동 실행 (같은 스크립트)
|
||||
- **스냅샷 경로**: `/volume1/incus/.snapshots/incus-YYYYMMDD`
|
||||
- **타입**: 읽기 전용 (`btrfs subvolume snapshot -r`)
|
||||
- **보관**: 3일 초과 자동 삭제
|
||||
- **용도**: 논리적 복구 (실수 삭제/덮어쓰기), NAS 내부에서만 유효
|
||||
|
||||
### 3. R2 sync (NAS → Cloudflare R2)
|
||||
|
||||
- **호스트**: Synology NAS (192.168.205.100)
|
||||
- **systemd**: `r2-incus-backup.timer` / `r2-incus-backup.service`
|
||||
- **스케줄**: 매일 06:00 (±5분)
|
||||
- **소스**: `/volume1/incus/`
|
||||
- **대상**: R2 버킷 `incus-backup` (APAC 리전)
|
||||
- **제외**: `.snapshots/**`, `@eaDir/**`
|
||||
- **rclone**: Docker (`rclone/rclone:latest`), 설정 `/volume1/docker/rclone/rclone.conf`
|
||||
- **성능**: transfers 32, checkers 32, 첫 전체 업로드(7.97GB) 약 12분
|
||||
- **TimeoutStartSec**: 3600 (1시간)
|
||||
|
||||
### NAS 설정 메모
|
||||
|
||||
- NAS `/volume1/incus/inbest/` 소유자: `kaffa:users` (rsync 쓰기용)
|
||||
- btrfs subvolume: `/volume1/incus` (ID 741)
|
||||
- 모든 백업 스크립트에 NAS 접근 불가 시 스킵/로컬 보관 로직 포함
|
||||
- NFS 마운트는 반드시 `soft,timeo=50,retrans=3` 사용 (hard 금지, [[nas-storage]] 참조)
|
||||
|
||||
### 복구 시나리오
|
||||
|
||||
| 상황 | 복구 방법 |
|
||||
|------|----------|
|
||||
| 파일 실수 삭제/덮어쓰기 | NAS btrfs 스냅샷에서 복원 |
|
||||
| NAS 장애 | R2에서 rclone copy로 복원 |
|
||||
| kr2 장애 | NAS rsync 미러에서 복원 |
|
||||
|
||||
## NocoDB PostgreSQL 백업
|
||||
|
||||
NocoDB가 사용하는 PostgreSQL(Incus 컨테이너) 백업. pg_dump → NAS.
|
||||
|
||||
### 1. pg_dump (kr2)
|
||||
|
||||
- **호스트**: incus-kr2
|
||||
- **스크립트**: `/usr/local/bin/pg-backup.sh`
|
||||
- **systemd**: `pg-backup.timer` / `pg-backup.service`
|
||||
- **스케줄**: 매일 03:00
|
||||
- **DB**: `10.100.2.5` (Incus 컨테이너 PostgreSQL)
|
||||
- **인증**: `PGPASSWORD=nocodb`, user `nocodb`, db `nocodb`
|
||||
- **출력**: `/mnt/nas-backup/daily/nocodb_YYYYMMDD_HHMMSS.dump` (NAS NFS 마운트)
|
||||
- **보관**: 30일 초과 자동 삭제
|
||||
- **NAS 마운트**: 스크립트 내에서 `soft,timeo=50,retrans=3`으로 자동 마운트
|
||||
- **NAS 미접근 시**: 10초 타임아웃 후 스킵
|
||||
|
||||
## kine 백업 (Supabase PostgreSQL)
|
||||
|
||||
K3s datastore인 Supabase PostgreSQL의 kine 테이블 백업. pg_dump → NAS → R2 3단계.
|
||||
|
||||
### 1. pg_dump (kr2)
|
||||
|
||||
- **호스트**: incus-kr2
|
||||
- **스크립트**: `/usr/local/bin/kine-backup.sh`
|
||||
- **systemd**: `kine-backup.timer` / `kine-backup.service`
|
||||
- **스케줄**: 매일 03:30 (±5분)
|
||||
- **DB**: Supabase Pooler (`aws-1-ap-southeast-1.pooler.supabase.com`)
|
||||
- **인증**: Vault `secret/cloud/supabase`
|
||||
- **출력**: `/opt/kine-backup/kine-YYYYMMDD.sql.gz`
|
||||
- **보관**: 7일 초과 자동 삭제
|
||||
- **크기**: ~9.3MB (gzip)
|
||||
|
||||
### 2. rsync → NAS (같은 스크립트)
|
||||
|
||||
- pg_dump 완료 직후 자동 실행 (같은 스크립트)
|
||||
- **대상**: `kaffa@192.168.205.100:/volume1/k3s-backup/kine/`
|
||||
- **SSH 키**: `/home/kaffa/.ssh/id_ed25519`
|
||||
|
||||
### 3. R2 sync (기존 r2-backup.timer로 자동 포함)
|
||||
|
||||
NAS `/volume1/k3s-backup/` → R2 `k3s-backup` 버킷 (매일 05:00)
|
||||
|
||||
### 복구 방법
|
||||
|
||||
```bash
|
||||
# NAS 또는 R2에서 덤프 파일 가져온 후
|
||||
gunzip kine-YYYYMMDD.sql.gz
|
||||
psql "$DB_URL" < kine-YYYYMMDD.sql
|
||||
```
|
||||
|
||||
## etcd 스냅샷 백업 (비활성)
|
||||
|
||||
K3s가 kine(PostgreSQL)으로 전환되어 etcd 백업은 불필요. etcd-backup.timer, etcd-backup-sync.timer 비활성화됨. kine 백업이 대체.
|
||||
|
||||
### 1. etcd snapshot (hp2 etcd 컨테이너)
|
||||
|
||||
- **호스트**: incus-hp2, etcd 컨테이너
|
||||
- **스크립트**: `/usr/local/bin/etcd-backup.sh`
|
||||
- **systemd**: `etcd-backup.timer` / `etcd-backup.service`
|
||||
- **스케줄**: 매일 03:30 (±5분)
|
||||
- **출력**: `/backup/etcd-YYYYMMDD_HHMMSS.db` (컨테이너) = `/opt/etcd-backup/` (호스트)
|
||||
- **보관**: 7일 초과 자동 삭제
|
||||
- **크기**: ~65MB
|
||||
|
||||
### 2. rsync → NAS (hp2 호스트)
|
||||
|
||||
- **호스트**: incus-hp2
|
||||
- **스크립트**: `/usr/local/bin/etcd-backup-sync.sh`
|
||||
- **systemd**: `etcd-backup-sync.timer` / `etcd-backup-sync.service`
|
||||
- **스케줄**: 매일 04:00 (±5분)
|
||||
- **소스**: `/opt/etcd-backup/`
|
||||
- **대상**: `kaffa@192.168.9.100:/volume1/k3s-backup/etcd/`
|
||||
- **SSH 키**: `/home/kaffa/.ssh/id_ed25519`
|
||||
- NAS IP: hp2에서는 192.168.9.100 사용 (kr2에서는 192.168.205.100)
|
||||
|
||||
### 3. R2 sync (기존 r2-backup.timer로 자동 포함)
|
||||
|
||||
NAS `/volume1/k3s-backup/` → R2 `k3s-backup` 버킷 (매일 05:00)
|
||||
|
||||
## OpenWrt 라우터 백업
|
||||
|
||||
- **호스트**: openwrt-gw (root@100.66.60.66)
|
||||
- **스크립트**: `/usr/local/bin/backup-openwrt.sh`
|
||||
- **스케줄**: cron 매일 03:30
|
||||
- **방식**: `sysupgrade -b /tmp/backup-openwrt.tar.gz` → scp → NAS
|
||||
- **대상**: `kaffa@192.168.9.100:/volume1/k3s-backup/openwrt/`
|
||||
- **SSH 키**: `/root/.ssh/id_ed25519` (Dropbear, dbclient 또는 `ssh -i` 필요)
|
||||
- **보관**: 7일 초과 자동 삭제
|
||||
- **크기**: ~18KB
|
||||
- **포함**: `/etc/` 전체 (haproxy.cfg, nftables.d/, config/firewall, config/network, crontabs/, ssh 키 등)
|
||||
- **복원**: `sysupgrade -r backup.tar.gz`
|
||||
|
||||
## k3s 백업 (기존)
|
||||
|
||||
- **호스트**: Synology NAS
|
||||
- **systemd**: `r2-backup.timer` / `r2-backup.service`
|
||||
- **스케줄**: 매일 05:00 (±5분)
|
||||
- **소스**: `/volume1/k3s-backup/`
|
||||
- **대상**: R2 버킷 `k3s-backup` (ENAM 리전)
|
||||
- **rclone**: Docker, transfers 4, checkers 8
|
||||
227
infra/data/k3s-backup.md
Normal file
227
infra/data/k3s-backup.md
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: K3s 백업 파이프라인
|
||||
updated: 2026-04-15 Longhorn 라벨 키 오타 수정 (recurring-job-group, 대시 포함). 자세히는 history/2026-04-15-longhorn-backup-label-typo.md 참조
|
||||
tags: [infra, backup, k3s, r2, longhorn, synology]
|
||||
---
|
||||
|
||||
## 아키텍처 (2026-04-14 이후)
|
||||
|
||||
두 레이어 병행:
|
||||
|
||||
```
|
||||
앱 레벨: K3s Pod(dump/rsync) → NAS (/volume1/k3s-backup/) → R2(k3s-backup)
|
||||
↓
|
||||
Synology systemd timer
|
||||
|
||||
Longhorn: Longhorn Volume → Snapshot → Backup → R2(longhorn-backup)
|
||||
↓
|
||||
RecurringJob (cluster-native)
|
||||
```
|
||||
|
||||
| 레이어 | 용도 | 정합성 | 복원 단위 |
|
||||
|--------|------|--------|----------|
|
||||
| 앱 레벨 (pg_dump, gitea dump, rsync) | 1순위 복구 경로 | application-consistent | 특정 테이블·파일 |
|
||||
| Longhorn Volume backup | 2순위 안전망, 볼륨 통째 롤백 | crash-consistent (block-level) | 볼륨 전체 |
|
||||
|
||||
**DB는 반드시 앱 레벨 dump 병행.** Longhorn 백업 단독은 crash-consistent에 불과해 트랜잭션 중간 상태가 캡처될 수 있음.
|
||||
|
||||
## K3s → NAS (NFS)
|
||||
|
||||
### NFS PV/PVC
|
||||
|
||||
| 네임스페이스 | PVC 이름 | NFS 경로 | 비고 |
|
||||
|-------------|----------|----------|------|
|
||||
| gitea | gitea-backup-nfs | /volume1/k3s-backup/gitea | gitea dump |
|
||||
|
||||
- NFS 서버: 192.168.9.100 (Synology NAS)
|
||||
- PV reclaim policy: Retain
|
||||
|
||||
### CronJob
|
||||
|
||||
| 네임스페이스 | CronJob | 스케줄 | 내용 |
|
||||
|-------------|---------|--------|------|
|
||||
| gitea | gitea-backup | 0 3 * * * (UTC) | gitea dump → NFS |
|
||||
|
||||
## NAS → R2 (Synology systemd timer)
|
||||
|
||||
### Synology 구성
|
||||
|
||||
- **rclone 설정**: `/volume1/docker/rclone/rclone.conf`
|
||||
- **rclone 실행**: Docker 컨테이너 (`rclone/rclone:latest`)
|
||||
- **systemd service**: `r2-backup.service`
|
||||
- **systemd timer**: `r2-backup.timer` — 매일 05:00 KST (±5분 jitter)
|
||||
|
||||
### R2 버킷
|
||||
|
||||
- 버킷명: `k3s-backup`
|
||||
- 엔드포인트: `https://d8e5997eb4040f8b489f09095c0f623c.r2.cloudflarestorage.com`
|
||||
- 크레덴셜: [[vault]] `secret/cloud/cloudflare/r2`
|
||||
|
||||
### 관리 명령 (Synology SSH)
|
||||
|
||||
```bash
|
||||
# 수동 sync
|
||||
sudo systemctl start r2-backup.service
|
||||
|
||||
# 상태 확인
|
||||
sudo systemctl status r2-backup.timer
|
||||
sudo systemctl list-timers r2-backup.timer
|
||||
|
||||
# 로그
|
||||
sudo journalctl -u r2-backup.service
|
||||
|
||||
# R2 내용 확인
|
||||
sudo /usr/local/bin/docker run --rm \
|
||||
-v /volume1/docker/rclone:/config:ro \
|
||||
rclone/rclone:latest \
|
||||
--config /config/rclone.conf \
|
||||
ls r2:k3s-backup/
|
||||
```
|
||||
|
||||
## 보존 정책
|
||||
|
||||
- NAS: 수동 관리 (디스크 여유에 따라)
|
||||
- R2: lifecycle rule 설정 필요 (Cloudflare 대시보드에서 30일 만료 설정)
|
||||
|
||||
---
|
||||
|
||||
# Longhorn 볼륨 백업 (2026-04-14 구축)
|
||||
|
||||
## Backup Target
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 종류 | Cloudflare R2 (S3 호환) |
|
||||
| 버킷 | `longhorn-backup` (APAC 리전) |
|
||||
| BackupTarget CR | `longhorn-system/default` → `s3://longhorn-backup@auto/` |
|
||||
| 크레덴셜 | K8s Secret `longhorn-system/longhorn-backup-r2` (Vault `secret/cloud/cloudflare/r2` 기반) |
|
||||
| Secret 키 | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_ENDPOINTS` |
|
||||
| 상태 확인 | `kubectl -n longhorn-system get backuptarget default` — AVAILABLE=true 여야 함 |
|
||||
|
||||
## RecurringJob (4개)
|
||||
|
||||
| 이름 | 그룹 | Task | Cron | 보존 | Concurrency |
|
||||
|------|------|------|------|------|-------------|
|
||||
| critical-snapshot | critical | snapshot | `0 * * * *` UTC (매시간) | 24 (1일치) | 2 |
|
||||
| critical-backup | critical | backup | `0 */6 * * *` UTC (6h 간격, KST 03/09/15/21시 포함) | 28 (7일치) | 1 |
|
||||
| standard-snapshot | standard | snapshot | `0 18 * * *` UTC (= KST 03:00) | 7 (7일치) | 2 |
|
||||
| standard-backup | standard | backup | `0 19 * * *` UTC (= KST 04:00) | 7 (7일치) | 1 |
|
||||
|
||||
> cron 은 UTC 기준. standard 그룹은 KST 새벽 트래픽 저점에 실행되도록 설정 (2026-04-15 조정).
|
||||
|
||||
> [!info] 보존 정책 통일 (2026-04-14)
|
||||
> 백업 보존을 일관적으로 **7일 기준**으로 통일. snapshot은 CoW 체인이라 개수보다 보존 기간이 중요 — critical은 1일치 시간 단위, standard는 7일치 일단위로 유지.
|
||||
|
||||
## 볼륨 분류 (2026-04-14 기준, 라벨 `recurring-job-group.longhorn.io/<group>=enabled`)
|
||||
|
||||
> [!warning] 라벨 키 주의 (2026-04-15 정정)
|
||||
> 정확한 키는 `recurring-job-group.longhorn.io/<group>` (**대시 포함**). 2026-04-14 초기 구축 시 `recurringjob-group.longhorn.io/<group>` (대시 없음) 오타로 18볼륨 백업이 전부 noop 동작했음. RecurringJob 컨트롤러는 셀렉터 매칭 실패 시 에러 없이 "Found 0 volumes"로 조용히 종료하므로 라벨 키는 반드시 대시 포함 형식으로 부착할 것.
|
||||
|
||||
### critical (13 볼륨)
|
||||
|
||||
APISIX etcd ×3, Gitea PostgreSQL, Gitea 저장소, VictoriaMetrics, n8n, OpenMemory data, OpenMemory qdrant, Outline, Portainer, SafeLine DB, Teleport.
|
||||
|
||||
### standard (5 볼륨)
|
||||
|
||||
Gitea Valkey, VictoriaLogs (14d 보존), Grafana, RabbitMQ, SFTPGo.
|
||||
|
||||
### 백업 제외
|
||||
|
||||
SafeLine 런타임 로그/상태 6종 (휘발성 OK), nfs-provisioner 마운트 (10Mi 시스템).
|
||||
|
||||
### 신규 볼륨 분류 절차
|
||||
|
||||
```bash
|
||||
vol=$(kubectl -n <ns> get pvc <pvc-name> -o jsonpath='{.spec.volumeName}')
|
||||
kubectl -n longhorn-system label volume $vol \
|
||||
recurring-job-group.longhorn.io/critical=enabled --overwrite
|
||||
# 또는 standard
|
||||
```
|
||||
|
||||
## 복원 절차 (**중요**: PVC annotation 방식 안 됨)
|
||||
|
||||
Longhorn v1.8+ CSI 동적 프로비저닝에서 `longhorn.io/fromBackup` PVC annotation은 **무시됨** (CSI가 annotation을 드라이버로 전달 안 함). 반드시 Volume CR 경유:
|
||||
|
||||
1. **Volume CR 생성** (`fromBackup` 스펙)
|
||||
```yaml
|
||||
apiVersion: longhorn.io/v1beta2
|
||||
kind: Volume
|
||||
metadata:
|
||||
name: restored-<tag>
|
||||
namespace: longhorn-system
|
||||
spec:
|
||||
fromBackup: "s3://longhorn-backup@auto/?backup=<backup-name>&volume=<orig-vol>"
|
||||
numberOfReplicas: 3
|
||||
size: "<bytes>"
|
||||
frontend: blockdev
|
||||
accessMode: rwo
|
||||
```
|
||||
|
||||
2. **복원 완료 대기** — `state=detached` 또는 `state=attached` + `restoreRequired=false`
|
||||
```bash
|
||||
kubectl -n longhorn-system get volume restored-<tag> \
|
||||
-o jsonpath='{.status.state} {.status.restoreRequired}'
|
||||
```
|
||||
|
||||
3. **PV 생성** (StorageClass는 `longhorn-static` — 동적 provisioning 회피)
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata: { name: restored-<tag>-pv }
|
||||
spec:
|
||||
capacity: { storage: <size> }
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: longhorn-static
|
||||
csi:
|
||||
driver: driver.longhorn.io
|
||||
fsType: ext4
|
||||
volumeHandle: restored-<tag>
|
||||
volumeAttributes: { numberOfReplicas: "3", staleReplicaTimeout: "30" }
|
||||
```
|
||||
|
||||
4. **PVC 생성** (PV에 직접 바인딩)
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata: { name: restored-<tag>-pvc, namespace: <ns> }
|
||||
spec:
|
||||
storageClassName: longhorn-static
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources: { requests: { storage: <size> } }
|
||||
volumeName: restored-<tag>-pv
|
||||
```
|
||||
|
||||
5. **Pod에 마운트** — 데이터 검증 후 원본 PVC로 swap (앱 재배치 필요)
|
||||
|
||||
> [!info] 2026-04-14 검증
|
||||
> 1Gi 볼륨에 10MB urandom + marker 파일 작성 → snap+backup → 삭제 → 복원 → MD5 완벽 일치 확인.
|
||||
|
||||
## 운영 명령
|
||||
|
||||
```bash
|
||||
# BackupTarget 상태
|
||||
kubectl -n longhorn-system get backuptarget default
|
||||
|
||||
# 볼륨별 백업 이력
|
||||
kubectl -n longhorn-system get backupvolume
|
||||
|
||||
# 최근 백업 목록
|
||||
kubectl -n longhorn-system get backups --sort-by='.metadata.creationTimestamp'
|
||||
|
||||
# 특정 볼륨의 수동 백업 트리거
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: longhorn.io/v1beta2
|
||||
kind: Snapshot
|
||||
metadata: { name: manual-$(date +%s), namespace: longhorn-system }
|
||||
spec: { volume: <volume-name>, createSnapshot: true }
|
||||
EOF
|
||||
```
|
||||
|
||||
Longhorn UI: https://longhorn.inouter.com → Backup 탭에서 시각적 확인 가능.
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] **R2 lifecycle rule** — `longhorn-backup` 버킷에 7일 만료 규칙 `longhorn-backup-7d-expire` 등록 완료 (2026-04-14, API 경유). `deleteObjectsTransition.condition.maxAge=604800`. Longhorn retention(7일)과 정합.
|
||||
- [ ] vault, openmemory, anvil, ironclad 등 추가 서비스 NFS 백업 CronJob 구성
|
||||
- [ ] postgres 백업 CronJob NFS 경로 `/volume1/k3s-backup/postgres`로 변경
|
||||
- [ ] 분기별 복원 드릴 (critical 그룹 1건 선정하여 실제 복원 절차 실행)
|
||||
338
infra/data/nas-storage.md
Normal file
338
infra/data/nas-storage.md
Normal file
@@ -0,0 +1,338 @@
|
||||
---
|
||||
title: NAS StorageClass (NFS + iSCSI)
|
||||
updated: 2026-04-14 문서 정확성 보정 (hp2 연결 상태, nodeAffinity, iSCSI on-demand 동작)
|
||||
tags: [infra, k3s, storage, nfs, iscsi, synology]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
Synology NAS(DS916+)를 K3s NFS/iSCSI StorageClass로 사용. Longhorn(로컬 블록)과 병행하여 파일 저장소, 웹소스 등 대용량/RWX 워크로드에 사용.
|
||||
|
||||
## NAS 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 모델 | Synology DS916+ (Braswell, x86_64, Linux 3.10.108) |
|
||||
| bond0 (eth0+eth1, 1G×2) | **192.168.9.100** — 관리/SMB/DSM 웹 |
|
||||
| eth2 (USB 2.5G, RTL8157, r8152) | **192.168.205.100** — NFS/iSCSI 데이터 플레인, MTU 9000 |
|
||||
| 디스크 | 11TB (사용 2%) |
|
||||
| NFS export | `/volume1/k3s-nfs` |
|
||||
| NFS 옵션 | `rw,async,no_wdelay,crossmnt,no_root_squash,insecure_locks,sec=sys,anonuid=1025,anongid=100` |
|
||||
|
||||
K3s 노드는 모두 192.168.205.0/24 서브넷에 참여하고 있음 (2026-04-14 검증):
|
||||
|
||||
| 노드 | 출구 NIC | src IP |
|
||||
|------|---------|--------|
|
||||
| incus-kr1 | enp7s0 (PCIe 2.5G) | 192.168.205.214 |
|
||||
| incus-kr2 | enx803f5dd34c9f (USB 2.5G) | 192.168.205.135 |
|
||||
| incus-hp2 | ens2 | 192.168.205.134 |
|
||||
|
||||
## K3s 설정
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| StorageClass | `nfs` (default 아님, 명시 지정 필요) |
|
||||
| Provisioner | nfs-subdir-external-provisioner (Helm) |
|
||||
| Namespace | `nfs-provisioner` |
|
||||
| NFS 경로 | `/volume1/k3s-nfs` |
|
||||
| 마운트 옵션 | `soft,timeo=50,retrans=3` |
|
||||
| archiveOnDelete | true (PVC 삭제 시 데이터 archived- 접두사로 보존) |
|
||||
| nodeAffinity | **없음 (제약 없이 배포)** — 2026-04-14 기준 Deployment에 nodeSelector/affinity 미설정. 3 노드(kr1/kr2/hp2) 전부 192.168.205.0/24 도달 가능해 스케줄 자유 |
|
||||
|
||||
### Helm 설치 명령
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner
|
||||
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
|
||||
--namespace nfs-provisioner --create-namespace \
|
||||
--set nfs.server=192.168.205.100 \
|
||||
--set nfs.path=/volume1/k3s-nfs \
|
||||
--set storageClass.name=nfs \
|
||||
--set storageClass.defaultClass=false \
|
||||
--set storageClass.reclaimPolicy=Delete \
|
||||
--set storageClass.archiveOnDelete=true \
|
||||
--set nfs.mountOptions[0]=soft \
|
||||
--set nfs.mountOptions[1]=timeo=50 \
|
||||
--set nfs.mountOptions[2]=retrans=3
|
||||
```
|
||||
|
||||
### PVC 사용 예시
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: my-data
|
||||
spec:
|
||||
storageClassName: nfs
|
||||
accessModes: [ReadWriteMany]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
## 성능 (fio 벤치마크, 2026-04-05)
|
||||
|
||||
| 테스트 | IOPS | 대역폭 | 레이턴시 |
|
||||
|--------|------|--------|---------|
|
||||
| Random Read 4K | 20.6K | 84 MB/s | 1.5 ms |
|
||||
| Random Write 4K | 18.1K | 74 MB/s | 1.8 ms |
|
||||
| Seq Read 1M | 263 | 276 MB/s | 30 ms |
|
||||
| Seq Write 1M | 275 | 288 MB/s | 29 ms |
|
||||
|
||||
## 용도별 StorageClass 선택
|
||||
|
||||
| 용도 | StorageClass | 이유 |
|
||||
|------|-------------|------|
|
||||
| DB (PostgreSQL, etcd, Redis) | longhorn | 저레이턴시 블록 필요 |
|
||||
| 파일 업로드, 사용자 데이터 | nfs | 대용량, RWX 지원 |
|
||||
| 웹소스, 정적 파일 | nfs | 대용량, 여러 Pod 공유 |
|
||||
| 로그, 임시 데이터 | longhorn/local-path | 빠른 쓰기 |
|
||||
|
||||
## 파일 소유권
|
||||
|
||||
- `no_root_squash` 설정으로 root 컨테이너는 소유권 문제 없음
|
||||
- 비-root 컨테이너는 Pod `securityContext.fsGroup`으로 제어
|
||||
|
||||
## NFS hard vs soft
|
||||
|
||||
- **hard**: NAS 끊기면 무한 대기 → 서버 먹통
|
||||
- **soft**: 타임아웃 후 에러 반환 → 서버 생존
|
||||
|
||||
모든 NFS 마운트는 `soft,timeo=50,retrans=3` 필수. 인시던트 이력: [[2026-04-04-usb-25g-hang|history]]
|
||||
|
||||
## iSCSI StorageClass (democratic-csi)
|
||||
|
||||
Synology NAS의 iSCSI를 K3s 블록 스토리지로 사용. democratic-csi가 PVC 생성/삭제 시 자동으로 iSCSI Target + LUN을 관리.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| StorageClass | `synology-iscsi` |
|
||||
| CSI Driver | democratic-csi (Helm) |
|
||||
| Namespace | `democratic-csi` |
|
||||
| iSCSI Portal | 192.168.205.100:3260 |
|
||||
| Base IQN | `iqn.2000-01.com.synology:NAS.k3s.` |
|
||||
| LUN 타입 | BLUN (Btrfs thin provisioning) |
|
||||
| Volume | /volume1 |
|
||||
| 인증 | kaffa 계정 (HTTPS API) |
|
||||
| 자동 관리 | PVC 생성 → Target+LUN 생성, PVC 삭제 → Target+LUN 삭제 |
|
||||
|
||||
### Helm 설치
|
||||
|
||||
values 파일: `/tmp/democratic-csi-values.yaml` (kr1)
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
helm repo add democratic-csi https://democratic-csi.github.io/charts/
|
||||
helm install synology-iscsi democratic-csi/democratic-csi \
|
||||
--namespace democratic-csi --create-namespace \
|
||||
-f /tmp/democratic-csi-values.yaml
|
||||
```
|
||||
|
||||
### ScsiTarget 서비스의 on-demand 동작 (중요)
|
||||
|
||||
DSM의 `ScsiTarget` 패키지는 LIO(kernel configfs) 기반이며, **Target이 0개이면 3260 포트를 LISTEN하지 않는다.** 이건 고장이 아니라 LIO의 정상 동작.
|
||||
|
||||
- PVC 없는 상태 → `netstat -lnt | grep 3260` 비어있음
|
||||
- PVC 생성 → democratic-csi가 Synology API로 Target+LUN 자동 생성 → configfs에 Target 등록 → 3260 자동 LISTEN
|
||||
- PVC 삭제 → Target/LUN 자동 제거 → Target 0개 되면 3260 닫힘
|
||||
|
||||
따라서 "3260 포트가 안 열려있다"만으로 장애 판단 금지. **Target 존재 여부 먼저 확인:**
|
||||
|
||||
```bash
|
||||
# NAS에서
|
||||
find /sys/kernel/config/target/iscsi -maxdepth 2 -type d
|
||||
sudo synopkg status ScsiTarget # "running"이어야 함
|
||||
```
|
||||
|
||||
### 2026-04-14 smoke test 결과
|
||||
|
||||
| 단계 | 결과 |
|
||||
|------|------|
|
||||
| PVC 생성 (1Gi, synology-iscsi) → Bound | 30초 |
|
||||
| Target + LUN 자동 생성 | OK (democratic-csi → Synology HTTPS API) |
|
||||
| 3260 LISTEN | OK (Target 등록과 동시에) |
|
||||
| discovery (kr1/kr2/hp2 전부) | OK (양 포털 192.168.205.100 + 192.168.9.100) |
|
||||
| PVC 삭제 → PV Released → DeleteVolume → PV gone | OK |
|
||||
|
||||
2026-04-14 이전 며칠간 ScsiTarget이 stop 상태로 방치되어 있었음. 원인 미확인(`/var/log/synopkg.log` 로그 기준 3/24 부팅 이후 stop 기록 없지만 실제 상태는 stop). 수동 `sudo synopkg start ScsiTarget`로 복구. 실제 소비자 PVC는 계속 0건이라 서비스 영향 없었음.
|
||||
|
||||
### 전체 StorageClass 요약
|
||||
|
||||
| StorageClass | 방식 | 용도 | HA |
|
||||
|---|---|---|---|
|
||||
| longhorn | 로컬 NVMe 블록 | DB, 고성능 블록 | 노드 간 레플리카 |
|
||||
| synology-iscsi | NAS iSCSI 블록 | 블록 스토리지 (NAS) | RAID5 |
|
||||
| nfs | NAS NFS 파일 | 파일, 웹소스, RWX | RAID5 |
|
||||
| local-path | 로컬 디스크 | 캐시, 임시 | 없음 |
|
||||
|
||||
## Jumbo Frame (MTU 9000) 설정 (2026-04-05)
|
||||
|
||||
모든 NAS 전용 경로에 MTU 9000 적용. 스위치 포함 전체 경로가 JF 지원.
|
||||
|
||||
| 장비 | 인터페이스 | MTU | 설정 위치 |
|
||||
|------|-----------|-----|----------|
|
||||
| kr1 | enp7s0 (PCIe 2.5GbE) | 9000 | `/etc/systemd/network/20-enp7s0.network` |
|
||||
| kr2 | enx803f5dd34c9f (USB 2.5GbE) | 9000 | `/etc/systemd/network/30-usb-2g5.network` |
|
||||
| hp2 | ens2 | 9000 | (설정 위치 확인 필요) |
|
||||
| NAS | eth2 (USB 2.5GbE) | 9000 | Synology DSM 네트워크 설정 |
|
||||
|
||||
### 2026-04-14 end-to-end JF 검증
|
||||
|
||||
`ping -M do -s 8972`(DF bit, 9000B 프레임) 전 구간 통과:
|
||||
|
||||
| 경로 | 결과 | avg RTT |
|
||||
|------|------|---------|
|
||||
| kr1 → NAS | 2/2 | 0.46ms |
|
||||
| kr2 → NAS | 2/2 | 0.44ms |
|
||||
| hp2 → NAS | 2/2 | 0.47ms |
|
||||
| kr1 → kr2 | 2/2 | 1.09ms |
|
||||
| kr1 → hp2 | 2/2 | 0.54ms |
|
||||
| hp2 → kr2 | 2/2 | 0.46ms |
|
||||
|
||||
스위치 포함 전 경로 9000B fragmentation 없이 통과. 노드 간(kr1↔kr2, kr1↔hp2 등)도 JF 동작 — storage 전용망 외 노드 간 통신에도 활용 가능.
|
||||
|
||||
## NAS eth2 USB NIC watchdog (2026-04-14)
|
||||
|
||||
NAS의 eth2(USB 2.5G, RTL8157)가 USB 3.0 LPM exit latency 미보고 이슈로 간헐적 disconnect 발생 (21일 uptime 기준 2회). 드라이버는 자동 reconnect하지만 인터페이스가 DOWN 상태로 남아 네트워크 서비스 중단.
|
||||
|
||||
근본 원인: DSM 3.10.108 커널의 xHCI 호스트 컨트롤러가 LPM `U1/U2 exit latency` descriptor를 OS에 노출하지 않음 → 링크 절전 복귀 타이밍 불일치 → `-71 EPROTO` → USB disconnect. DS916+ 하드웨어/커널 조합의 근본적 한계로 드라이버 업데이트·커널 교체로 해결 불가.
|
||||
|
||||
### 대응: cron watchdog
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 스크립트 | `/usr/local/bin/eth2-watchdog.sh` (root:755) |
|
||||
| 주기 | 1분 (root cron, `/etc/crontab`) |
|
||||
| 로그 | `/var/log/eth2-watchdog.log` |
|
||||
| 영속성 | `/dev/md0` ext4 root, 재부팅 유지 (DSM 메이저 업그레이드 시 재확인 필요) |
|
||||
|
||||
동작:
|
||||
1. `/sys/class/net/eth2/operstate` 확인
|
||||
2. `up`이 아니면 `ip link set eth2 up` (IP는 DSM ifcfg-eth2가 자동 할당)
|
||||
3. UP 후 IP 없으면 `ip addr add 192.168.205.100/24 dev eth2`
|
||||
4. MTU ≠ 9000 이면 보정
|
||||
|
||||
### 검증 기록
|
||||
|
||||
- 2026-04-14 강제 down 테스트: 1분 내 watchdog 실행 → `ip link up` → DSM ifcfg 자동 IP 할당 → 정상 복귀 (MTU 9000 유지)
|
||||
- Outline 상세 기록: `2026-04-14 / NAS eth2 watchdog 구축 / kappa` (id `93baf66b-f003-47a9-9d14-7a66e3dbfde0`)
|
||||
- RCA 문서: `2026-04-14 / NAS eth2 USB NIC 링크 드롭 원인 조사 / kappa` (id `db923170-8c16-459d-82ce-46fdc1f0f0d0`)
|
||||
|
||||
### kr2 USB NIC 드라이버 (r8152 DKMS)
|
||||
|
||||
kr2의 USB 2.5GbE 어댑터(Realtek RTL8157, 0bda:8157)는 커널 기본 `cdc_ncm` 드라이버로 잡히면 Half Duplex + MTU 1500 제한. DKMS r8152 드라이버(v2.21.4)를 설치하여 Full Duplex + Jumbo Frame 지원.
|
||||
|
||||
- DKMS 패키지: `linux-headers-$(uname -r)` 설치 시 자동 빌드
|
||||
- udev 규칙: `/etc/udev/rules.d/50-usb-realtek-net.rules` — USB config를 1로 설정하여 r8152가 바인딩
|
||||
- 커널 업데이트 시 DKMS가 자동으로 재빌드
|
||||
|
||||
### 주의사항
|
||||
|
||||
- Jumbo Frame은 경로 전체(NIC → 스위치 → NIC)가 지원해야 함. 미지원 스위치가 중간에 있으면 프레임 드롭
|
||||
- `ping -M do -s 8972` 로 end-to-end JF 동작 확인 가능
|
||||
|
||||
## DSM Reverse Proxy — `nas.inouter.com`
|
||||
|
||||
LAN 전용으로 Synology DSM 웹 UI 접근. `nas.inouter.com` 은 `*.inouter.com` 와일드카드 폴백으로 `k3s.inouter.com (192.168.9.53, Traefik MetalLB VIP)` 로 해석되고, Traefik 에서 이 호스트를 Synology NAS 로 reverse proxy 한다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Namespace | `lan-proxies` (신규 — LAN-only 리버스 프록시 전용) |
|
||||
| Service | `nas` (selector 없는 ClusterIP) + EndpointSlice → `192.168.9.100:5000` |
|
||||
| Backend | Synology DSM HTTP (포트 5000, 평문) |
|
||||
| Traefik IngressRoute | `nas` (web entrypoint, redirect → https), `nas-tls` (websecure entrypoint) |
|
||||
| Middleware | `redirect-https` (`redirectScheme.scheme=https permanent=true`) |
|
||||
| TLS | 기존 wildcard `wildcard-inouter-tls` (cert-manager + emberstack reflector 자동 복제) |
|
||||
| 외부 노출 | LAN-only — 와일드카드 `*.inouter.com` 가 사설 IP `192.168.9.53` 로 폴백되므로 인터넷 라우팅 없음 |
|
||||
|
||||
### ExternalName 대신 Service+EndpointSlice 사용 이유
|
||||
|
||||
요구사항은 "ExternalName Service" 였으나 Kubernetes ExternalName 은 RFC1123 DNS 호스트네임만 허용하고 IP(`192.168.9.100`)는 거부. 동등한 K8s-native 패턴인 **selector-less Service + 수동 EndpointSlice** 로 구현. CoreDNS 가 ClusterIP 를 정상 발행하고 kube-proxy 가 EndpointSlice 의 외부 IP 로 분산.
|
||||
|
||||
### 매니페스트
|
||||
|
||||
원본: `/tmp/nas-reverse-proxy.yaml` (heimdall). 핵심 발췌 — 전체 6 객체.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: lan-proxies
|
||||
labels:
|
||||
purpose: lan-only-reverse-proxy
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata: { name: nas, namespace: lan-proxies }
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- { name: dsm-http, port: 5000, targetPort: 5000, protocol: TCP }
|
||||
---
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
kind: EndpointSlice
|
||||
metadata:
|
||||
name: nas
|
||||
namespace: lan-proxies
|
||||
labels: { kubernetes.io/service-name: nas }
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- { name: dsm-http, port: 5000, protocol: TCP }
|
||||
endpoints:
|
||||
- addresses: [192.168.9.100]
|
||||
conditions: { ready: true }
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata: { name: redirect-https, namespace: lan-proxies }
|
||||
spec:
|
||||
redirectScheme: { scheme: https, permanent: true }
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata: { name: nas, namespace: lan-proxies }
|
||||
spec:
|
||||
entryPoints: [web]
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: Host(`nas.inouter.com`)
|
||||
middlewares: [{ name: redirect-https }]
|
||||
services: [{ name: nas, port: 5000 }]
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata: { name: nas-tls, namespace: lan-proxies }
|
||||
spec:
|
||||
entryPoints: [websecure]
|
||||
routes:
|
||||
- kind: Rule
|
||||
match: Host(`nas.inouter.com`)
|
||||
services: [{ name: nas, port: 5000 }]
|
||||
tls:
|
||||
secretName: wildcard-inouter-tls
|
||||
```
|
||||
|
||||
### 동작 검증
|
||||
|
||||
```bash
|
||||
curl -sk --resolve nas.inouter.com:80:192.168.9.53 http://nas.inouter.com/ # → 301 https://nas.inouter.com/
|
||||
curl -sk --resolve nas.inouter.com:443:192.168.9.53 https://nas.inouter.com/ # → 200, 12094B
|
||||
# <title>NAS - Synology DiskStation</title>
|
||||
```
|
||||
|
||||
### 주의사항
|
||||
|
||||
- **LAN-only**. `*.inouter.com` 가 사설 192.168.9.53 으로 가는 한 외부 인터넷에서는 도달 불가. 외부 노출 시 보안/인증 재검토 필요 (Synology DSM 직노출은 CVE 노출면 큼).
|
||||
- DSM HTTPS(5001) 도 동일 호스트에서 200 응답하지만 자기서명 인증서 + 이중 TLS 종료라 백엔드는 평문 5000 사용.
|
||||
- Traefik 리버스 프록시 패턴은 `lan-proxies` ns 에 추가 LAN 서비스(예: 스위치/라우터/NAS 부속 UI) 적층 가능.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[backup]] — 백업 파이프라인 (NAS 활용)
|
||||
- [[storage-plan]] — NVMe NAS + iSCSI 기획, 벤치마크 결과
|
||||
- [[postgresql-ha]] — PostgreSQL HA (Patroni + etcd)
|
||||
- [[infra-hosts]] — 서버 목록
|
||||
- [[cert-manager]] — `wildcard-inouter-tls` 발급/복제
|
||||
- [[cloudflare]] — `nas.inouter.com` DNS 이력
|
||||
195
infra/data/postgresql-ha.md
Normal file
195
infra/data/postgresql-ha.md
Normal file
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: PostgreSQL HA (Patroni + etcd)
|
||||
updated: 2026-04-06
|
||||
tags: [infra, postgresql, patroni, etcd, ha]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
PostgreSQL 3노드 HA 클러스터. Patroni가 자동 failover를 관리하고, etcd를 DCS(Distributed Consensus Store)로 사용.
|
||||
|
||||
K3s의 kine 데이터스토어로 사용 중.
|
||||
|
||||
## PostgreSQL 클러스터
|
||||
|
||||
| 노드 | 호스트 | IP | 역할 |
|
||||
|------|--------|-----|------|
|
||||
| postgres-1 | incus-hp2 | 10.100.2.5 | Replica 또는 Leader |
|
||||
| postgres-2 | incus-kr1 | 10.100.3.185 | Replica 또는 Leader |
|
||||
| postgres-3 | incus-kr2 | 10.100.1.83 | Replica 또는 Leader |
|
||||
|
||||
- PostgreSQL 17.9, Patroni 4.1.0
|
||||
- Patroni 설정: `/etc/patroni.yml`
|
||||
- Patroni 서비스: `/etc/systemd/system/patroni.service` (ExecStart: `/opt/patroni/bin/patroni /etc/patroni.yml`)
|
||||
- 클러스터 이름: `nocodb-cluster`
|
||||
- 레플리케이션: async streaming
|
||||
- Patroni REST API: 각 노드 8008 포트
|
||||
- 컨테이너 메모리 제한: 2GiB (limits.memory)
|
||||
- shared_buffers: 512MB, effective_cache_size: 1536MB, work_mem: 8MB, maintenance_work_mem: 128MB, wal_buffers: 16MB
|
||||
|
||||
### DB 목록
|
||||
|
||||
| DB | 용도 |
|
||||
|----|------|
|
||||
| kine | K3s 데이터스토어 (kine) |
|
||||
| nocodb | NocoDB |
|
||||
| n8n | n8n 워크플로 |
|
||||
| outline | Outline 위키 |
|
||||
|
||||
### Patroni 명령어
|
||||
|
||||
```bash
|
||||
# 클러스터 상태 확인
|
||||
incus exec postgres-1 -- /opt/patroni/bin/patronictl -c /etc/patroni.yml list
|
||||
|
||||
# 수동 switchover
|
||||
incus exec postgres-1 -- /opt/patroni/bin/patronictl -c /etc/patroni.yml switchover
|
||||
|
||||
# Replica reinitialize
|
||||
incus exec postgres-1 -- /opt/patroni/bin/patronictl -c /etc/patroni.yml reinit nocodb-cluster postgres-3 --force
|
||||
```
|
||||
|
||||
## etcd 클러스터 (Patroni DCS)
|
||||
|
||||
| 노드 | 위치 | IP | 방식 |
|
||||
|------|------|-----|------|
|
||||
| etcd-nas | Synology NAS (서울) | 192.168.9.100 | Docker (`quay.io/coreos/etcd:v3.5.21`) |
|
||||
| etcd-mbp | kaffa-macbookpro (서울, Tailscale) | 100.115.154.78 | Docker (`quay.io/coreos/etcd:v3.5.17`) via colima, peer/client는 socat으로 Tailscale 노출 |
|
||||
| etcd-jp1 | Incus 컨테이너 jp1 (도쿄) | 10.253.101.233 | Alpine + `apk add etcd` (v3.5.16) |
|
||||
|
||||
- NAS: 데이터 `/volume1/docker/etcd/data`, `--restart=always`
|
||||
- jp1: openrc 서비스 (`/etc/init.d/etcd`), `command_background=true`
|
||||
- mbp: docker container `etcd` (named volume `etcd-data`), client/peer 모두 호스트 `127.0.0.1`만 노출 → `socat`이 Tailscale IP `100.115.154.78`의 `2379`/`2380`으로 forward (`~/Library/LaunchAgents/com.kaffa.etcd-socat{,-peer}.plist`)
|
||||
- Patroni etcd namespace: `/patroni`
|
||||
|
||||
### etcd 확인 명령어
|
||||
|
||||
```bash
|
||||
# 클러스터 멤버 확인
|
||||
incus exec postgres-1 -- etcdctl --endpoints=http://192.168.9.100:2379 member list -w table
|
||||
|
||||
# 엔드포인트 상태
|
||||
incus exec postgres-1 -- etcdctl --endpoints=http://192.168.9.100:2379,http://100.115.154.78:2379,http://10.253.101.233:2379 endpoint status -w table
|
||||
```
|
||||
|
||||
### etcd에 저장된 데이터
|
||||
|
||||
| prefix | 용도 |
|
||||
|--------|------|
|
||||
| `/patroni` | Patroni DCS (Leader election, 설정) |
|
||||
| `/apisix/osaka` | APISIX 오사카 라우팅 설정 |
|
||||
| `/apisix/tokyo` | APISIX sandbox-tokyo 라우팅 설정 (미사용, 데이터 보존) |
|
||||
| `/apisix/seoul` | APISIX 서울 K3s 라우팅 설정 |
|
||||
|
||||
## K3s kine 연결
|
||||
|
||||
K3s → **pgx multi-host** → Patroni 3노드 직결 (HAProxy 미경유).
|
||||
|
||||
kine 은 `github.com/jackc/pgx/v5` 드라이버 사용 — libpq 호환 multi-host + `target_session_attrs=read-write` 지원. Patroni failover 시 pgx 가 자동으로 새 primary 에 재연결 (API 다운타임 <1초).
|
||||
|
||||
### K3s config
|
||||
|
||||
```yaml
|
||||
# /etc/rancher/k3s/config.yaml (kr1, kr2)
|
||||
datastore-endpoint: "postgres://kine:kine@10.100.2.5:5432,10.100.3.185:5432,10.100.1.83:5432/kine?target_session_attrs=read-write&sslmode=disable"
|
||||
```
|
||||
|
||||
이전 구성 (HAProxy 경유, 2026-04-16 폐기): `postgres://kine:kine@192.168.9.1:5432/kine`
|
||||
|
||||
### 검증 (2026-04-16)
|
||||
|
||||
kr2 → kr1 순서로 rolling restart. switchover (postgres-1→postgres-3, TL12→13) 시 kine API 다운타임 **<1초** (t+2ms 에 정상 응답). [[2026-04-16-kine-multihost-migration|history]]
|
||||
|
||||
### OpenWrt HAProxy :5432 프론트엔드
|
||||
|
||||
kine + pgpool 전환 완료로 HAProxy postgres 프론트엔드는 **더 이상 트래픽 없음**. 1주 관측 후 제거 예정.
|
||||
|
||||
## 애플리케이션 접속 경로 — pgpool-II
|
||||
|
||||
모든 K3s 내부 애플리케이션(NocoDB, n8n, Outline)은 **pgpool-II**를 통해 Patroni 3노드에 직접 접속. HAProxy 미경유.
|
||||
|
||||
```
|
||||
NocoDB/n8n/Outline → pgpool.db.svc.cluster.local:9999 → Patroni 3노드 직결
|
||||
```
|
||||
|
||||
이전 pgcat+HAProxy 구조는 2026-04-16 폐기. [[2026-04-16-pgpool-full-migration|history]]
|
||||
|
||||
### pgpool 구성
|
||||
|
||||
- 이미지: `pgpool/pgpool:4.4.3` (공식, DockerHub 최신 태그)
|
||||
- K8s 매니페스트: `kaffa/helm-charts` repo `pgpool/` 디렉토리 (ArgoCD `pgpool` Application, selfHeal+prune)
|
||||
- replicas=2, `podAntiAffinity` (topologyKey `kubernetes.io/hostname`)
|
||||
- `PodDisruptionBudget` `minAvailable: 1`
|
||||
- Service: ClusterIP `pgpool.db.svc.cluster.local:9999`
|
||||
|
||||
### pgpool.conf 핵심
|
||||
|
||||
```
|
||||
backend_clustering_mode = 'streaming_replication'
|
||||
|
||||
backend_hostname0/1/2 = 10.100.2.5 / 10.100.3.185 / 10.100.1.83
|
||||
backend_weight = 1, backend_flag = ALLOW_TO_FAILOVER
|
||||
|
||||
sr_check_user = 'sr_check' (REPLICATION + pg_monitor 역할, pg에 생성 필요)
|
||||
sr_check_period = 10
|
||||
health_check_period = 10
|
||||
|
||||
failover_on_backend_error = off (Patroni가 promotion 수행)
|
||||
failover_command = ''
|
||||
follow_primary_command = ''
|
||||
use_watchdog = off
|
||||
|
||||
load_balance_mode = off (모든 쿼리 primary — read-your-write 일관성)
|
||||
num_init_children = 32
|
||||
max_pool = 4
|
||||
connection_cache = on
|
||||
pool_passwd = '' (미사용 — 아래 인증 참조)
|
||||
```
|
||||
|
||||
### 인증
|
||||
|
||||
Postgres `password_encryption = scram-sha-256` cluster-wide.
|
||||
|
||||
- `allow_clear_text_frontend_auth = on` + pool_hba method `password` → 클라이언트 plaintext 전송 → pgpool 이 backend scram-sha-256 challenge-response 에 직접 사용
|
||||
- `pool_passwd = ''` — 비활성. pgpool 이 plaintext pool_passwd 엔트리를 자동 md5 해시하여 backend scram 거절되는 문제 회피
|
||||
- K8s Service 내부 트래픽이라 clear-text 는 클러스터 내에서만 노출
|
||||
|
||||
### Patroni TCP keepalive (유지)
|
||||
|
||||
```
|
||||
tcp_keepalives_idle = 60
|
||||
tcp_keepalives_interval = 10
|
||||
tcp_keepalives_count = 3
|
||||
```
|
||||
|
||||
pgpool 은 자체 연결 관리가 있어 클라이언트 좀비 문제 없지만, Postgres → pgpool 역방향 dead socket 감지용으로 Patroni 파라미터 유지.
|
||||
|
||||
### Patroni switchover 통합 테스트 (2026-04-16)
|
||||
|
||||
3 서비스 전체 pgpool 경유 상태에서 `patronictl switchover --force`:
|
||||
|
||||
- write 복구: **즉시** (t+3ms)
|
||||
- n8n: 3건 transient error · NocoDB: 0건 · Outline: 0건
|
||||
- 전 서비스 HTTP 200 유지
|
||||
|
||||
이전 pgcat 대비: 동일 시나리오에서 n8n 1038건 / NocoDB 13건 / pod restart 필요
|
||||
|
||||
마이그레이션 이력: [[2026-04-16-pgpool-n8n-poc|PoC]] · [[2026-04-16-pgpool-full-migration|전면 전환]]
|
||||
|
||||
## APISIX etcd 사용 현황
|
||||
|
||||
| 사이트 | etcd | prefix | 비고 |
|
||||
|--------|------|--------|------|
|
||||
| osaka | 통합 클러스터 (192.168.9.100, ...) | `/apisix/osaka` | Docker APISIX (waf-apisix), [[apisix#오사카-apisix-osaka]] |
|
||||
| sandbox-tokyo | (미가동) | `/apisix/tokyo` | 2026-04-08 NixOS 전환으로 APISIX 자체 폐기, etcd 데이터만 보존 |
|
||||
| 서울 K3s | **K3s 내부 apisix-etcd StatefulSet** (apisix.apisix.svc:2379) | `/apisix` | 2026-04-08 외부 통합에서 K3s 내부로 복귀 |
|
||||
|
||||
APISIX etcd 통합/분리 이력: [[2026-04-06-apisix-etcd-consolidation|history]]. 현재 외부 통합 etcd는 **Patroni DCS + osaka APISIX 전용**.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[nas-storage]] — NAS StorageClass, Jumbo Frame 설정
|
||||
- [[storage-plan]] — NVMe NAS + iSCSI 기획
|
||||
- [[backup]] — 백업 파이프라인
|
||||
- [[infra-hosts]] — 서버 목록
|
||||
- [[k3s-migration]] — K3s 마이그레이션 기록
|
||||
98
infra/data/sftpgo.md
Normal file
98
infra/data/sftpgo.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: SFTPGo (SFTP/SSH 서버)
|
||||
updated: 2026-03-27
|
||||
tags: [infra, sftp, ssh, k3s]
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
SFTPGo - 고객용 SFTP/SSH/WebDAV 서버. 사용자 관리, 키 관리, 웹 파일 관리 UI 내장.
|
||||
|
||||
## 배포 정보
|
||||
|
||||
K3s 클러스터(kr3 컨텍스트)에 Helm으로 설치 (2026-03-27)
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Namespace | sftpgo |
|
||||
| Chart | oci://ghcr.io/sftpgo/helm-charts/sftpgo 0.44.0 |
|
||||
| App | SFTPGo 2.7.1 |
|
||||
| SFTP | LoadBalancer 192.168.9.55:22 |
|
||||
| Web UI | 8080 (Traefik HTTPRoute 경유) |
|
||||
| Storage (DB) | Longhorn 1Gi (SQLite) |
|
||||
| Storage (파일) | R2 `sftpgo-storage` 버킷 (APAC), 사용자별 S3 filesystem |
|
||||
| R2 API 키 | Vault `secret/cloud/r2-sftpgo` |
|
||||
| 관리자 | Vault `secret/apps/sftpgo` (admin_user, admin_password) |
|
||||
|
||||
## 외부 접근 경로
|
||||
|
||||
### 웹 UI (HTTPS)
|
||||
```
|
||||
sftp.inouter.com → Cloudflare (proxied) → 220.120.65.245
|
||||
→ OpenWrt HAProxy :443 → Traefik 192.168.9.53:443
|
||||
→ HTTPRoute → SFTPGo 8080
|
||||
```
|
||||
|
||||
### SFTP (포트 22)
|
||||
```
|
||||
sftp.inouter.com:22 (또는 52.79.45.166:22)
|
||||
→ relay4wd:22 → iptables REDIRECT :9022
|
||||
→ APISIX stream_route → 192.168.9.55:22
|
||||
→ SFTPGo Pod
|
||||
```
|
||||
|
||||
## 웹 UI
|
||||
|
||||
| URL | 용도 |
|
||||
|-----|------|
|
||||
| https://sftp.inouter.com/web/admin | 관리자 - 사용자/설정 관리 |
|
||||
| https://sftp.inouter.com/web/client | 고객 - 파일 업로드/다운로드, 패스워드 변경 |
|
||||
|
||||
## R2 백엔드 설정
|
||||
|
||||
사용자별 S3 filesystem으로 R2에 파일 저장. 고객이 SFTP로 업로드하면 R2에 직접 저장됨.
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 버킷 | `sftpgo-storage` (APAC) |
|
||||
| 엔드포인트 | `https://d8e5997eb4040f8b489f09095c0f623c.r2.cloudflarestorage.com` |
|
||||
| API 키 | Vault `secret/cloud/r2-sftpgo` (access_key_id, secret_access_key) |
|
||||
| 사용자별 경로 | `key_prefix: {username}/` |
|
||||
| provider | 1 (S3) |
|
||||
|
||||
사용자 생성 시 filesystem.provider를 1로 설정하고 s3config에 위 정보를 넣으면 됨.
|
||||
|
||||
## 주요 설정
|
||||
|
||||
| 설정 | 값 | 이유 |
|
||||
|------|-----|------|
|
||||
| `token_validation` | 1 (IP 바인딩 없음) | Cloudflare proxy 경유 시 IP 변동으로 토큰 거부 방지 |
|
||||
| `proxy_allowed` | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | Traefik → SFTPGo 프록시 헤더 신뢰 |
|
||||
|
||||
## API
|
||||
|
||||
```bash
|
||||
# 토큰 발급 (패스워드: Vault secret/apps/sftpgo)
|
||||
TOKEN=$(curl -s https://sftp.inouter.com/api/v2/token -u 'kaffa:패스워드' | jq -r .access_token)
|
||||
|
||||
# 사용자 목록
|
||||
curl -s -H "Authorization: Bearer $TOKEN" https://sftp.inouter.com/api/v2/users
|
||||
|
||||
# 사용자 생성 (R2 백엔드)
|
||||
curl -s -X POST https://sftp.inouter.com/api/v2/users \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"user1","password":"pass","status":1,"home_dir":"/srv/sftpgo/data/user1","permissions":{"/":["*"]},
|
||||
"filesystem":{"provider":1,"s3config":{
|
||||
"bucket":"sftpgo-storage","region":"auto",
|
||||
"access_key":"(Vault에서 확인)",
|
||||
"access_secret":{"status":"Plain","payload":"(Vault에서 확인)"},
|
||||
"endpoint":"https://d8e5997eb4040f8b489f09095c0f623c.r2.cloudflarestorage.com",
|
||||
"key_prefix":"user1/","force_path_style":true}}}'
|
||||
```
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[apisix]] - relay4wd stream_route로 SFTP 포워딩
|
||||
- [[sshpiper]] - SSH 리버스 프록시 (내부 서버 접근용)
|
||||
- [[teleport]] - 관리자 SSH/K8s 접근 관리
|
||||
92
infra/data/storage-plan.md
Normal file
92
infra/data/storage-plan.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: 스토리지 기획 — NVMe NAS + iSCSI
|
||||
updated: 2026-04-05
|
||||
tags: [infra, storage, plan, iscsi, nas]
|
||||
---
|
||||
|
||||
## 현재 구성
|
||||
|
||||
| StorageClass | 용도 | 장비 |
|
||||
|---|---|---|
|
||||
| longhorn | DB, 블록 스토리지 | 각 노드 로컬 NVMe |
|
||||
| synology-iscsi | NAS 블록 스토리지 | Synology NAS (HDD, democratic-csi) |
|
||||
| nfs | 파일, 웹소스, RWX | Synology NAS (HDD 11TB RAID5) |
|
||||
| local-path | 캐시, 임시 | 각 노드 로컬 디스크 |
|
||||
|
||||
### 네트워크 현황 (2026-04-05)
|
||||
|
||||
- 2.5GbE, MTU 9000 (Jumbo Frame), Full Duplex
|
||||
- kr1: PCIe NIC (enp7s0)
|
||||
- kr2: USB NIC (RTL8157, r8152 DKMS 드라이버)
|
||||
- iperf3 실측: 단방향 2.35-2.38 Gbps
|
||||
|
||||
### MariaDB 벤치마크 (sysbench OLTP, 4테이블 × 10만건)
|
||||
|
||||
| 테스트 | Longhorn (로컬 NVMe) | Synology iSCSI (HDD, 2.5GbE) |
|
||||
|---|---|---|
|
||||
| OLTP R/W 4t TPS | 800 | 437 |
|
||||
| OLTP R/W 16t TPS | 2,260 | 1,351 |
|
||||
| OLTP Read Only 16t TPS | 4,732 | 2,222 |
|
||||
|
||||
### fio 디스크 벤치마크 (iSCSI 경유)
|
||||
|
||||
| 테스트 | IOPS | 대역폭 | Avg 레이턴시 |
|
||||
|---|---|---|---|
|
||||
| Random Read 4K | 19.1K | 78 MB/s | 1.67ms |
|
||||
| Random Write 4K | 18.2K | 75 MB/s | 1.75ms |
|
||||
| Seq Read 1M | 240 | 253 MB/s | 16.6ms |
|
||||
| Seq Write 1M | 264 | 277 MB/s | 15.1ms |
|
||||
|
||||
## 미래 계획: NVMe NAS + 2.5G + iSCSI
|
||||
|
||||
Longhorn을 NVMe NAS iSCSI로 대체하는 구성.
|
||||
|
||||
### 도입 장비
|
||||
|
||||
- NVMe NAS (4디스크 RAID5)
|
||||
- 2.5GbE 네트워크
|
||||
- UPS (기 보유)
|
||||
|
||||
### 예상 성능 (2.5G iSCSI)
|
||||
|
||||
| 항목 | 예상 | 현재 Longhorn (로컬 NVMe) |
|
||||
|---|---|---|
|
||||
| Sequential | ~300 MB/s | 3.9 GB/s |
|
||||
| Random 4K | 50-80K IOPS | 500K IOPS |
|
||||
| 레이턴시 | 0.3-0.5 ms | 0.06 ms |
|
||||
|
||||
현재 서비스 규모에서는 차이 체감 어려움. DB 워크로드 실용적 사용 가능.
|
||||
|
||||
### K3s 연동
|
||||
|
||||
- CSI 드라이버: democratic-csi (Synology iSCSI 지원)
|
||||
- StorageClass로 등록하여 PVC로 사용
|
||||
- 블록 디바이스로 노출되므로 fsync, direct I/O 정상 동작
|
||||
|
||||
### HA/안정성
|
||||
|
||||
| 보호 | 방법 |
|
||||
|---|---|
|
||||
| 디스크 고장 | RAID5 (4디스크, 1개 고장 허용) |
|
||||
| 정전 | UPS |
|
||||
| NAS 하드웨어 고장 | kine 백업(Supabase)으로 클러스터 복구, 볼륨 데이터는 R2 백업에서 복원 |
|
||||
|
||||
### 기존 Synology (HDD) 역할 변경
|
||||
|
||||
NVMe NAS 도입 후 기존 HDD NAS는:
|
||||
- 백업 저장소 (현재와 동일)
|
||||
- NFS 대용량 파일 저장소
|
||||
- R2 동기화 중간 스테이지
|
||||
|
||||
### 전환 조건
|
||||
|
||||
- 2.5G 네트워크 구성 완료
|
||||
- NVMe NAS 장비 확보
|
||||
- democratic-csi + iSCSI 테스트 완료
|
||||
- 기존 Longhorn 볼륨 마이그레이션 계획 수립
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [[nas-storage]] — 현재 NFS StorageClass 설정
|
||||
- [[backup]] — 백업 파이프라인
|
||||
- [[infra-hosts]] — 서버 목록
|
||||
Reference in New Issue
Block a user