# K3s-specific 예시 모든 config 파일과 manifest는 1000+ 서비스 production 기준. YAML은 `kubectl apply --server-side --dry-run=server` 통과. config.yaml은 `k3s server --help`와 공식 docs에 대응하는 키만 사용. --- ## 좋은 예시 1: prod server config.yaml (disable 세트 + audit + etcd snapshot S3) ```yaml # /etc/rancher/k3s/config.yaml # Single source of truth, identically applied to every server node via Ansible/CI. write-kubeconfig-mode: "0640" # --- Cluster network (must match on ALL server nodes) --- cluster-cidr: "10.42.0.0/16" service-cidr: "10.43.0.0/16" cluster-dns: "10.43.0.10" cluster-domain: "cluster.local" flannel-backend: "vxlan" # --- Disable packaged components (prod defaults) --- disable: - traefik - servicelb - local-storage disable-cloud-controller: false disable-network-policy: false disable-helm-controller: false # --- TLS SAN for kube-apiserver cert --- tls-san: - "k3s.prod.example.internal" - "10.0.1.10" - "10.0.1.11" - "10.0.1.12" # --- Audit logging --- kube-apiserver-arg: - "audit-log-path=/var/log/k3s/audit.log" - "audit-log-maxage=30" - "audit-log-maxbackup=10" - "audit-log-maxsize=100" - "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml" - "feature-gates=ServerSideApply=true" # --- Kubelet hardening --- kubelet-arg: - "config=/etc/rancher/k3s/kubelet.yaml" # --- etcd snapshot to S3 (every 6h, keep 72h) --- etcd-snapshot-schedule-cron: "0 */6 * * *" etcd-snapshot-retention: 12 etcd-s3: true etcd-s3-endpoint: "s3.ap-northeast-2.amazonaws.com" etcd-s3-bucket: "k3s-etcd-backups-prod" etcd-s3-region: "ap-northeast-2" etcd-s3-folder: "prod-cluster" # etcd-s3-access-key / etcd-s3-secret-key loaded from /etc/rancher/k3s/.env via systemd EnvironmentFile # --- Registries mirror (cluster-internal pull accelerator) --- # Not using embedded-registry (Spegel); using external Harbor mirror instead. # See /etc/rancher/k3s/registries.yaml. # --- Node labels / taints applied to this server's kubelet --- node-label: - "example.com/role=control-plane" - "example.com/environment=prod" node-taint: - "node-role.kubernetes.io/control-plane=:NoSchedule" ``` **왜 좋은가:** - `cluster-cidr` / `service-cidr` / `cluster-dns` / `cluster-domain` / `flannel-backend` / `disable` 세트가 Git 하나의 파일에 고정 → 다음 server 노드 조인 시 mismatch 불가 - audit log 설정이 kube-apiserver에 강제 주입됨 (SOC2/ISO27001 요구) - etcd snapshot이 6시간 주기 + S3 업로드로 DR 대비 - ssm key는 파일에 없고 systemd EnvironmentFile로 주입 (Secret을 Git에 박지 않음) --- ## 좋은 예시 2: config.yaml.d drop-in 분할 (역할별 파일) ```yaml # /etc/rancher/k3s/config.yaml.d/10-networking.yaml cluster-cidr: "10.42.0.0/16" service-cidr: "10.43.0.0/16" flannel-backend: "vxlan" ``` ```yaml # /etc/rancher/k3s/config.yaml.d/20-audit.yaml kube-apiserver-arg: - "audit-log-path=/var/log/k3s/audit.log" - "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml" ``` ```yaml # /etc/rancher/k3s/config.yaml.d/30-etcd-backup.yaml etcd-snapshot-schedule-cron: "0 */6 * * *" etcd-snapshot-retention: 12 etcd-s3: true etcd-s3-endpoint: "s3.ap-northeast-2.amazonaws.com" etcd-s3-bucket: "k3s-etcd-backups-prod" ``` **왜 좋은가:** - 역할별 파일 = 팀별 CODEOWNERS 분리 (네트워크 / 감사 / DR) - 변경 diff가 좁아짐 - K3s는 drop-in 파일들을 병합해서 로드 --- ## 좋은 예시 3: Traefik을 유지해야 할 때 (dev 클러스터) `HelmChartConfig` ```yaml apiVersion: helm.cattle.io/v1 kind: HelmChartConfig metadata: name: traefik namespace: kube-system labels: app.kubernetes.io/name: traefik app.kubernetes.io/instance: traefik-dev app.kubernetes.io/component: ingress-controller app.kubernetes.io/part-of: platform app.kubernetes.io/managed-by: argocd example.com/environment: dev spec: valuesContent: |- deployment: replicas: 2 ports: web: forwardedHeaders: trustedIPs: - 10.0.0.0/8 - 172.16.0.0/12 proxyProtocol: trustedIPs: - 10.0.0.0/8 websecure: tls: enabled: true resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi podSecurityContext: runAsNonRoot: true runAsUser: 65532 seccompProfile: type: RuntimeDefault metrics: prometheus: enabled: true serviceMonitor: enabled: true ``` **왜 좋은가:** - packaged manifest를 직접 수정하지 않음 - `metadata.name` + `namespace`가 K3s 생성 `HelmChart`와 일치 → override가 merge됨 - secret/TLS 민감값은 `valuesSecrets`로 분리 가능 (이 예시는 non-sensitive만 보여줌) - ServiceMonitor 활성화로 observability 자동 연결 --- ## 좋은 예시 4: embedded registry mirror (Spegel) opt-in + registries.yaml ```yaml # /etc/rancher/k3s/config.yaml (partial — applied to every node, server AND agent) embedded-registry: true ``` ```yaml # /etc/rancher/k3s/registries.yaml mirrors: docker.io: endpoint: - "https://harbor.prod.example.internal" registry.k8s.io: endpoint: - "https://harbor.prod.example.internal" "*": # Spegel will also share images between nodes via p2p configs: "harbor.prod.example.internal": auth: username: "robot$k3s-pull" password: "__HARBOR_PULL_TOKEN__" tls: insecure_skip_verify: false ca_file: "/etc/rancher/k3s/harbor-ca.crt" ``` **네트워크 전제 (반드시 검증):** ```bash # From each node, to every other node: nc -zv 5001 # Spegel p2p gossip nc -zv 6443 # Local registry + K3s supervisor ``` **왜 좋은가:** - 공식 문서 기준 포트 (`TCP 5001 + TCP 6443`) 정확히 반영 - external mirror (Harbor) + intra-cluster p2p 공유 조합 → airgap 경계 대비 - `embedded-registry: true`가 **모든 노드 (server+agent)의 config.yaml에 동일**하게 박혀야 함 --- ## 좋은 예시 5: local-path를 dev/test에만 제한 (StorageClass 레벨) ```yaml # local-path: default false, only used when explicitly requested apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-path annotations: storageclass.kubernetes.io/is-default-class: "false" labels: app.kubernetes.io/name: local-path app.kubernetes.io/instance: local-path-dev app.kubernetes.io/component: storage app.kubernetes.io/part-of: platform app.kubernetes.io/managed-by: argocd example.com/environment: dev example.com/storage-tier: local-ephemeral provisioner: rancher.io/local-path reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: longhorn-replicated annotations: storageclass.kubernetes.io/is-default-class: "true" labels: app.kubernetes.io/name: longhorn app.kubernetes.io/instance: longhorn-prod app.kubernetes.io/component: storage app.kubernetes.io/part-of: platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/storage-tier: replicated-persistent provisioner: driver.longhorn.io allowVolumeExpansion: true reclaimPolicy: Retain volumeBindingMode: WaitForFirstConsumer parameters: numberOfReplicas: "3" staleReplicaTimeout: "30" fromBackup: "" fsType: "ext4" dataLocality: "best-effort" ``` **왜 좋은가:** - `local-path`는 default가 아니고 `example.com/storage-tier: local-ephemeral`로 dev에서만 수용 - prod default는 Longhorn replicated (3 replica) + `reclaimPolicy: Retain` - DB/Vault/MinIO PVC는 `storageClassName: longhorn-replicated` 명시 --- ## 좋은 예시 6: registries.yaml에서 production pull-through mirror ```yaml # /etc/rancher/k3s/registries.yaml (every node) mirrors: docker.io: endpoint: - "https://harbor.prod.example.internal/v2/dockerhub-proxy" quay.io: endpoint: - "https://harbor.prod.example.internal/v2/quay-proxy" registry.k8s.io: endpoint: - "https://harbor.prod.example.internal/v2/k8s-proxy" ghcr.io: endpoint: - "https://harbor.prod.example.internal/v2/ghcr-proxy" configs: "harbor.prod.example.internal": tls: ca_file: "/etc/rancher/k3s/harbor-ca.crt" auth: username: "robot$k3s-pull" password: "__HARBOR_PULL_TOKEN__" ``` **왜 좋은가:** - public registry rate limit / downtime이 클러스터 pull을 못 죽임 - Harbor에서 CVE scan + image signing 검증 - 모든 노드에 동일 파일 (Ansible/Fleet push) --- ## 나쁜 예시 1: packaged traefik.yaml 직접 edit ```bash ssh k3s-server-1 sudo vim /var/lib/rancher/k3s/server/manifests/traefik.yaml # added forwardedHeaders.trustedIPs inline sudo systemctl restart k3s ``` **문제:** K3s는 재시작 시 이 파일을 packaged 기본값으로 overwrite한다. 커스터마이징이 조용히 사라지고 서버별로 drift까지 생긴다. `HelmChartConfig`만 허용되는 경로. --- ## 나쁜 예시 2: server 간 서로 다른 critical 플래그 ```yaml # k3s-server-1: /etc/rancher/k3s/config.yaml cluster-cidr: "10.42.0.0/16" disable: [ traefik, servicelb ] ``` ```yaml # k3s-server-2: /etc/rancher/k3s/config.yaml cluster-cidr: "10.44.0.0/16" # mismatched disable: [ traefik ] # mismatched ``` **문제:** `critical configuration value mismatch` 로 server-2의 join이 실패하거나, 최악의 경우 이전 값이 캐시되어 silent drift가 생긴다. critical 값은 **Git 하나의 파일**로 통일해야 한다. --- ## 나쁜 예시 3: embedded registry mirror를 켜고 firewall 포트 미개방 ```yaml # /etc/rancher/k3s/config.yaml (all nodes) embedded-registry: true ``` ```bash # On each node, firewalld / iptables only allows 6443, 10250, 8472 # Port 5001 is CLOSED between nodes ``` **문제:** Spegel은 **TCP 5001 (p2p) + TCP 6443 (registry + supervisor)** 양쪽이 모든 노드 간 reachable해야 한다. 5001이 막혀있으면 p2p gossip 실패로 image sharing이 작동하지 않고, pull 지연이 오히려 커진다. 공식 기준: `https://docs.k3s.io/installation/registry-mirror`. --- ## 나쁜 예시 4: 운영 AddOn을 서버마다 scp로 push ```bash scp ingress-custom.yaml k3s-server-1:/var/lib/rancher/k3s/server/manifests/ # forgot server-2 and server-3 ``` **문제:** K3s는 이 디렉터리를 server 간 동기화하지 않는다. 리더가 server-2로 바뀌면 AddOn이 사라진 것처럼 보인다. Git + ArgoCD/Flux가 단일 진입점이어야 한다. --- ## 나쁜 예시 5: prod postgres StatefulSet을 `local-path`에 배치 ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: data-postgres-0 namespace: prod-data-postgres spec: storageClassName: local-path accessModes: [ ReadWriteOnce ] resources: requests: storage: 200Gi ``` **문제:** local-path = 노드 hostPath. 노드가 죽으면 PVC 데이터도 죽는다. 스냅샷 불가, 복제 불가, 마이그레이션 불가. prod DB는 Longhorn replicated / Ceph RBD / 외부 CSI 필수. --- ## 나쁜 예시 6: Traefik 유지하면서 `HelmChartConfig` 이름을 잘못 박음 ```yaml apiVersion: helm.cattle.io/v1 kind: HelmChartConfig metadata: name: traefik-custom # WRONG: must match the packaged HelmChart name namespace: kube-system spec: valuesContent: |- deployment: replicas: 3 ``` **문제:** `HelmChartConfig`의 `metadata.name`은 K3s가 생성한 `HelmChart`와 **이름·namespace 모두 일치**해야 override가 merge된다. `traefik-custom`은 무시되고, override가 반영되지 않는다. 올바른 이름은 `traefik`.