k3s 단일 노드에서도 운영 가능한 Kubernetes를 만드는 방법

곽태욱·2026년 2월 15일
post-thumbnail

사이드 프로젝트 실제 구성으로 본 GitOps, Secret Supply Chain, Observability, DR 설계

쿠버네티스는 설치보다 운영이 어려워요. 특히 단일 노드 k3s는 시작은 쉽지만, 운영 관점에서는 SPOF·비밀관리·복구전략 같은 문제를 피할 수 없어요.

이번 글에서는 사이드 프로젝트 클러스터에서 실제로 적용한 구조를 중심으로, “홈랩을 넘어 운영 가능한 최소 플랫폼”을 어떻게 만들었는지 정리했어요.

1) 부트스트랩은 1회, 나머지는 전부 GitOps

핵심은 kubectl을 영구 운영 도구로 쓰지 않는 거예요. bootstrap 단계는 딱 한 번만 사람이 실행하고, 그 이후 상태 수렴은 전부 Argo CD가 담당해요.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/gwak2837/....git
    targetRevision: main
    path: k8s/argocd
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

이 패턴의 장점은 운영자 의존성을 줄인다는 점이에요. “누가 언제 클러스터에서 무엇을 고쳤는지” 대신 “Git commit이 무엇을 바꿨는지”로 사고할 수 있어요.

2) 의존성 그래프를 sync-wave로 명시해 부팅 순서를 제어

실무에서 가장 자주 깨지는 건 컴포넌트 간 선후관계예요. 이 구성은 순서를 명시적으로 모델링해요:

  • 0: Vault
  • 1: External Secrets Operator
  • 2: cloudflared / MinIO Operator
  • 3: monitoring / MinIO tenant
  • 4: Velero / 앱

예를 들어 Vault 앱은 멀티소스(Helm chart + Git values + 추가 리소스)와 wave를 함께 써요.

name: platform-vault
annotations:
  # Ensure Vault is created before ESO/apps that depend on it.
  argocd.argoproj.io/sync-wave: '0'
spec:
  project: platform
  sources:
    - repoURL: https://helm.releases.hashicorp.com
      chart: vault
      targetRevision: 0.32.0
      helm:
        releaseName: vault
        valueFiles:
          - $values/k8s/platform/vault/vault.values.yaml
    - repoURL: https://github.com/gwak2837/....git
      targetRevision: main
      ref: values
    - repoURL: https://github.com/gwak2837/....git
      targetRevision: main
      path: k8s/platform/vault
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true

여기서 중요한 포인트는 “구성 자체가 의존성 문서”가 된다는 거예요. 운영자가 머릿속으로 순서를 외우지 않아도 되고, 신규 온보딩도 빨라져요.

3) Secret Supply Chain: Vault를 SSOT로, ESO는 Projection 계층으로

이 환경은 Git에 시크릿을 커밋하지 않아요. Git에는 ExternalSecretSecretStore 같은 선언만 있고, 실제 값은 Vault에만 있어요.

또 하나 중요한 결정은 ClusterSecretStore를 비활성화한 점이에요. 권한 범위를 네임스페이스 단위로 강제해서 blast radius를 줄일 수 있어요.

installCRDs: true

# Reduce privileges/attack surface: we use namespaced SecretStore + ExternalSecret only.
processClusterStore: false
processClusterExternalSecret: false
processClusterGenerator: false
processClusterPushSecret: false

crds:
  createClusterSecretStore: false
  createClusterExternalSecret: false
  createClusterGenerator: false
  createClusterPushSecret: false

네임스페이스마다 eso-vault SA + Vault role을 분리해서 app-prod, app-stg, monitoring이 서로의 경로를 읽지 못해요.

4) 보안은 “기본값 신뢰”가 아니라 “경계 명시”로

이 프로젝트에서 보안은 선언적으로 설정됐어요.

  • 앱 Pod automountServiceAccountToken: false
  • 컨테이너 allowPrivilegeEscalation: false, capabilities.drop: [ALL], seccompProfile: RuntimeDefault
  • 네임스페이스 PSA restricted 강제
  • Redis는 NetworkPolicy로 ingress source를 app-web, app-backend로 제한

예외적으로 Velero node-agent는 hostPath/mountPropagation이 필요해서 네임스페이스를 privileged로 분리했어요.

labels:
  # Velero node-agent(filesystem backup)는 hostPath + mountPropagation을 사용해요.
  # 그래서 PSA는 별도 namespace에서 privileged로 분리하는 걸 권장해요.
  pod-security.kubernetes.io/enforce: privileged
  pod-security.kubernetes.io/audit: privileged
  pod-security.kubernetes.io/warn: privileged
  pod-security.kubernetes.io/enforce-version: latest
  pod-security.kubernetes.io/audit-version: latest
  pod-security.kubernetes.io/warn-version: latest

“전부 restricted”가 아니라 필요 최소 예외를 근거와 함께 남기는 형태예요.

5) k3s 현실에 맞춘 관측성과 오토스케일링

HPA가 돌려면 metrics pipeline이 살아 있어야 해요. k3s/온프레미스에서는 kubelet 인증서/주소 이슈로 metrics-server가 자주 깨지기 때문에, 이 구성은 patch를 넣어 현실적인 가용성을 택해요.

# k3s/온프레미스에서 kubelet 인증서/주소 문제로 metrics가 안 뜨는 경우가 많아서 기본 플래그를 추가해요.
- target:
    group: apps
    version: v1
    kind: Deployment
    name: metrics-server
    namespace: kube-system
  patch: |-
    - op: add
      path: /spec/template/spec/containers/0/args/-
      value: --kubelet-insecure-tls
    - op: add
      path: /spec/template/spec/containers/0/args/-
      value: --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname

kube-prometheus-stack 값에서 k3s 특성(컨트롤플레인 scrape 부재, EndpointSlice 전환)을 반영해 노이즈를 줄였어요. 이건 “모니터링을 붙였다”가 아니라 “모니터링이 운영을 방해하지 않게 튜닝했다”에 가까워요.

6) DR은 GitOps만으로 끝나지 않아요: control plane + data plane을 분리해서 백업

GitOps는 desired state 복원에는 강하지만, stateful data는 못 복원해요. 그래서 백업을 두 축으로 가져갔어요.

  1. k3s embedded etcd snapshot: API object 실제 상태 백업
  2. Velero + S3 호환 스토리지(MinIO): PV 데이터 백업

이 구조 덕분에 복구 시나리오를 두 가지로 운영할 수 있어요.

  • 새 클러스터 생성 → Velero restore → Argo CD 재동기화
  • GitOps 재배포 → 필요한 데이터만 선택 복구

7) 이 아키텍처의 트레이드오프

장점도 분명하지만 비용도 있어요.

  • Vault + ESO는 런타임 비밀관리 품질을 크게 높이지만, 초기 부트스트랩 절차가 복잡해져요.
  • 단일 노드 k3s는 운영비가 낮지만, 노드 장애 시 가용성은 포기해야 해요.
  • Argo CD auto-sync/self-heal은 drift를 줄이지만, 동적 필드가 많은 리소스는 OutOfSync 노이즈를 따로 다뤄야 해요.

즉, 이 구성의 본질은 “최소한의 컴포넌트로 운영 원칙을 먼저 세운 뒤, 노드 수와 장애도메인을 확장하는 전략”이에요.

마무리

좋은 Kubernetes 운영은 “도구를 많이 쓰는 것”이 아니라, 의존성·권한·복구 가능성을 선언적으로 설계하고 계속 검증하는 일이에요. 사이드 프로젝트 구성에서 배운 핵심은 세 가지예요.

  • 부트스트랩은 짧게, 운영은 GitOps로 길게
  • Secret은 Git 밖에서, 권한은 네임스페이스 단위로
  • 관측/백업은 설치가 아니라 장애 시나리오 기준으로

단일 노드에서도 여기까지 정리해 두면, 멀티노드/멀티클러스터로 확장할 때 “다시 설계”가 아니라 “스케일 확장”만 하면 돼요.

profile
이유와 방법을 알려주는 메모장 겸 블로그 (Frontend, AI, 경제, 책)

0개의 댓글