26S05y

SFTPGo Pod 배포 & AIStor(S3) 연동 구성 가이드

0. 아키텍처 개요 및 핵심 결정사항

[클라이언트] --FTP(21/2121)+Passive Data Port--> [SFTPGo Pod] --S3 API--> [AIStor]
                                                        |
                                              [메타데이터 DB: SQLite/PostgreSQL]

먼저 결정해야 할 것 3가지

  1. FTP passive mode 노출 방식: FTP는 제어 채널(21) 외에 데이터 채널을 매번 별도 포트로 협상합니다(passive mode). 쿠버네티스에서는 이 포트 range를 그대로 열어줘야 하므로, 일반 ClusterIP Service로는 동작하지 않습니다. → NodePort(범위 지정) 또는 hostNetwork/hostPort 방식이 필요합니다. (가능하다면 SFTP가 NAT/방화벽 친화적이라 더 낫지만, 요구사항이 FTP이므로 이 가이드는 FTP 기준으로 작성합니다.)
  2. 메타데이터 저장소(data provider): 기본은 SQLite(파일 기반)라 Pod가 1개(replica=1)로 제한되고 PVC가 필요합니다. 이중화/스케일 아웃이 필요하면 PostgreSQL 등 외부 DB를 권장합니다. 이 가이드는 단일 Pod + SQLite(PVC) 기준으로 작성하고, 외부 DB 전환 방법도 함께 안내합니다.
  3. S3 자격증명 관리: AIStor access key/secret은 반드시 Kubernetes Secret으로 관리하고, ConfigMap이나 평문 env로 노출하지 않습니다.

1. Namespace 및 Secret 생성

kubectl create namespace sftpgo

AIStor 접속 정보 Secret

kubectl -n sftpgo create secret generic aistor-s3-credentials \
  --from-literal=access_key='<AISTOR_ACCESS_KEY>' \
  --from-literal=access_secret='<AISTOR_SECRET_KEY>'

SFTPGo 초기 관리자 계정 Secret

kubectl -n sftpgo create secret generic sftpgo-admin-credentials \
  --from-literal=admin_user='admin' \
  --from-literal=admin_password='<강력한-비밀번호>'

2. ConfigMap: SFTPGo 설정 오버라이드

SFTPGo는 기본적으로 env var(SFTPGO_섹션__키 형태)로 대부분 설정을 오버라이드할 수 있습니다. FTP 활성화 + passive IP 설정이 핵심입니다.

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sftpgo-config
  namespace: sftpgo
data:
  SFTPGO_FTPD__BINDINGS__0__PORT: "2121"
  SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP: "<노드의 외부 접근 가능 IP 또는 LB IP>"
  SFTPGO_FTPD__PASSIVE_PORT_RANGE__START: "50000"
  SFTPGO_FTPD__PASSIVE_PORT_RANGE__END: "50100"
  # 데이터 제공자 (기본 SQLite 유지 시 아래 생략 가능)
  SFTPGO_DATA_PROVIDER__DRIVER: "sqlite"
  SFTPGO_DATA_PROVIDER__NAME: "/srv/sftpgo/data/sftpgo.db"
  # 최초 admin 자동 생성 (선택)
  SFTPGO_DEFAULT_ADMIN__USERNAME: "admin"

FORCE_PASSIVE_IP는 FTP passive mode의 핵심입니다. 클라이언트가 데이터 채널을 열 때 서버가 "이 IP로 접속해"라고 알려주는 값인데, Pod 내부 IP를 그대로 주면 외부에서 접근이 불가능합니다. NodePort를 쓴다면 해당 Node의 외부 IP, LoadBalancer를 쓴다면 그 LB의 IP를 지정해야 합니다.


3. Deployment

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sftpgo
  namespace: sftpgo
spec:
  replicas: 1               # SQLite 사용 시 반드시 1 유지
  strategy:
    type: Recreate           # PVC 단일 마운트이므로 롤링 업데이트 대신 Recreate
  selector:
    matchLabels:
      app: sftpgo
  template:
    metadata:
      labels:
        app: sftpgo
    spec:
      containers:
        - name: sftpgo
          image: drakkan/sftpgo:v2.6.7   # 배포 시점 최신 안정 버전으로 확인 후 지정 권장
          envFrom:
            - configMapRef:
                name: sftpgo-config
          env:
            - name: SFTPGO_DEFAULT_ADMIN__PASSWORD
              valueFrom:
                secretKeyRef:
                  name: sftpgo-admin-credentials
                  key: admin_password
          ports:
            - containerPort: 8080   # Web/REST API
            - containerPort: 2022   # SFTP (필요 없으면 제거 가능)
            - containerPort: 2121   # FTP 제어 채널
            - containerPort: 50000  # Passive 포트 range 시작
              # K8s ports는 단일 포트만 명시 가능하므로 range는 Service/방화벽에서 처리
          volumeMounts:
            - name: sftpgo-data
              mountPath: /srv/sftpgo/data
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1"
              memory: "1Gi"
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
      volumes:
        - name: sftpgo-data
          persistentVolumeClaim:
            claimName: sftpgo-data-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sftpgo-data-pvc
  namespace: sftpgo
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 5Gi

4. Service — FTP Passive Port 노출 (가장 까다로운 부분)

옵션 A. NodePort (권장, 온프레미스 환경)

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sftpgo
  namespace: sftpgo
spec:
  type: NodePort
  selector:
    app: sftpgo
  ports:
    - name: web
      port: 8080
      targetPort: 8080
    - name: ftp-control
      port: 2121
      targetPort: 2121
      nodePort: 30121
    - name: passive-50000
      port: 50000
      targetPort: 50000
      nodePort: 30000
    # NodePort Service는 포트를 하나씩 나열해야 함 — range 전체(50000~50100)를
    # 매핑하려면 101개 port 항목이 필요하거나, 아래 "옵션 B(hostNetwork)"가 현실적입니다.

중요: 표준 K8s Service는 포트 range를 한 번에 지정할 수 없습니다(각 포트를 개별 선언해야 함). Passive port를 50~100개 단위로 열려면 YAML이 매우 길어집니다. 실무에서는 아래 옵션 B(hostNetwork)를 더 많이 사용합니다.

옵션 B. hostNetwork + hostPort (실무 권장)

# deployment.yaml 의 spec.template.spec 에 추가
spec:
  hostNetwork: true
  dnsPolicy: ClusterFirstWithHostNet
  containers:
    - name: sftpgo
      ports:
        - containerPort: 2121
          hostPort: 2121
        - containerPort: 8080
          hostPort: 8080
      # passive range는 컨테이너 내부 -e 로 전달한 50000-50100과
      # 노드 자체의 방화벽 정책만 맞으면 hostNetwork 특성상 그대로 통과됩니다.
  • hostNetwork: true를 쓰면 Pod가 노드의 네트워크 스택을 직접 사용하므로 passive port range가 별도 매핑 없이 그대로 열립니다. 다만 해당 노드에는 SFTPGo Pod가 1개만 뜰 수 있고(포트 충돌), 노드 방화벽에서 50000-50100/tcp를 직접 열어야 합니다.
  • 이 경우 FORCE_PASSIVE_IP는 그 노드의 IP로 지정합니다.
  • nodeSelector로 특정 노드에 고정 배치하는 것을 권장합니다.
      nodeSelector:
        sftpgo-node: "true"

5. SFTPGo 사용자 생성 + S3(AIStor) 백엔드 연결 (초기화 Job)

관리자 계정이 뜬 뒤, REST API로 "AIStor를 백엔드로 쓰는 FTP 업로드 전용 사용자"를 자동 생성하는 Job입니다.

# init-user-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: sftpgo-init-user
  namespace: sftpgo
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: init-user
          image: curlimages/curl:8.10.1
          env:
            - name: ADMIN_USER
              valueFrom: { secretKeyRef: { name: sftpgo-admin-credentials, key: admin_user } }
            - name: ADMIN_PASS
              valueFrom: { secretKeyRef: { name: sftpgo-admin-credentials, key: admin_password } }
            - name: S3_ACCESS_KEY
              valueFrom: { secretKeyRef: { name: aistor-s3-credentials, key: access_key } }
            - name: S3_ACCESS_SECRET
              valueFrom: { secretKeyRef: { name: aistor-s3-credentials, key: access_secret } }
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -e
              BASE=http://sftpgo.sftpgo.svc.cluster.local:8080/api/v2

              # 1) 관리자 토큰 발급
              TOKEN=$(curl -s -u "$ADMIN_USER:$ADMIN_PASS" "$BASE/token" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')

              # 2) FTP 업로드용 사용자 생성 (S3 필터시스템, AIStor 엔드포인트 연결)
              curl -s -X POST "$BASE/users" \
                -H "Authorization: Bearer $TOKEN" \
                -H "Content-Type: application/json" \
                -d '{
                  "username": "gpu-uploader",
                  "password": "'"<업로드-계정-비밀번호>"'",
                  "status": 1,
                  "permissions": { "/": ["*"] },
                  "filesystem": {
                    "provider": 1,
                    "s3config": {
                      "bucket": "<AISTOR_BUCKET_NAME>",
                      "region": "us-east-1",
                      "access_key": "'"$S3_ACCESS_KEY"'",
                      "access_secret": "'"$S3_ACCESS_SECRET"'",
                      "endpoint": "https://<AISTOR_ENDPOINT_HOST>:<PORT>",
                      "force_path_style": true,
                      "key_prefix": "gpu-node-uploads/"
                    }
                  }
                }'

S3 config에서 특히 신경 써야 할 값

  • endpoint: AIStor의 S3 API 엔드포인트 (예: https://aistor.internal.example.com:9000). AWS S3가 아니므로 반드시 명시.
  • force_path_style: true: AIStor/MinIO 계열은 대부분 virtual-hosted style이 아닌 path-style(https://endpoint/bucket/key)을 요구합니다. 빠뜨리면 버킷 인식 실패가 흔한 오류입니다.
  • region: AIStor가 특정 region 문자열을 요구하지 않더라도 SDK가 빈 값을 거부하는 경우가 있어 us-east-1 등 임의 값을 넣는 것이 안전합니다.
  • key_prefix: 이 사용자가 버킷 내 특정 경로 하위에서만 동작하게 제한하고 싶을 때 사용 (일종의 chroot).

6. 보안/TLS 고려사항

  • FTP는 기본적으로 평문 프로토콜입니다. 인증정보와 파일 내용이 그대로 노출되므로, 가능하면 FTPS(TLS)를 활성화하세요:
  SFTPGO_FTPD__BINDINGS__0__TLS_MODE: "1"   # 1=explicit, 2=implicit

이 경우 인증서/키를 Secret으로 마운트하고 SFTPGO_FTPD__CERTIFICATE_FILE, SFTPGO_FTPD__CERTIFICATE_KEY_FILE 환경변수로 경로를 지정합니다.

  • GPU node → SFTPGo Pod 구간이 아직 private망이 아니라면(이전 논의 참고), 이 FTP 트래픽도 임시 경로(외부망)를 탈 가능성이 있습니다 — TLS 미적용 시 자격증명 평문 노출 리스크가 커지므로, 임시 구간에서는 FTPS 필수 적용을 권장합니다.
  • AIStor access key는 버킷 단위로 최소 권한(해당 업로드 버킷/prefix에만 PutObject, ListBucket 등)으로 발급하는 것을 권장합니다.

7. 배포 및 검증 절차

kubectl apply -f configmap.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl -n sftpgo rollout status deployment/sftpgo
kubectl apply -f init-user-job.yaml
kubectl -n sftpgo logs job/sftpgo-init-user

테스트 체크리스트

  • kubectl -n sftpgo get pods — Pod Running 상태 확인
  • Web Admin 접속 확인 (http://<node-ip>:8080/web/admin)
  • FTP 클라이언트(lftp, FileZilla 등)로 접속 테스트:
    lftp -u gpu-uploader,<비밀번호> ftp://<FORCE_PASSIVE_IP>:2121
    put testfile.bin
  • 업로드한 파일이 AIStor 버킷에서 실제로 확인되는지 (AIStor 콘솔 또는 mc ls/aws s3 ls로 직접 검증)
  • 대용량 파일(모델 체크포인트 크기 급) 업로드 시 passive port timeout 없이 완료되는지 확인
  • Pod 재시작 후에도 SQLite 데이터(PVC)와 사용자 설정이 유지되는지 확인
  • (TLS 적용 시) FTPS 접속이 인증서 오류 없이 되는지 확인

8. 향후 확장 시 고려사항

  • 다중 replica가 필요해지면: SQLite → PostgreSQL로 data provider 전환 (SFTPGO_DATA_PROVIDER__DRIVER=postgresql 등), StatefulSet 대신 다수의 stateless Deployment 운용 가능.
  • hostNetwork 대신 순수 K8s networking을 쓰고 싶다면: FTP 대신 SFTP(단일 포트 2022)로 전환하는 것을 재고해볼 가치가 있습니다 — GPU node에서 AIStor로의 업로드가 자동화 스크립트/파이프라인이라면 SFTP나 S3 API 직접 호출이 K8s 운영 관점에서 훨씬 단순합니다.
  • 모니터링: SFTPGo는 Prometheus /metrics 엔드포인트를 제공하므로 업로드 처리량/실패율을 모니터링 스택에 편입해두면 이후 Phase 4/5(Storage 연동 테스트, 성능 테스트)에서 유용합니다.

0개의 댓글