Files
project-infra/docs/examples/infra/k3s-specific.md
T

401 lines
12 KiB
Markdown

# 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 <other-node-ip> 5001 # Spegel p2p gossip
nc -zv <other-node-ip> 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`.