401 lines
12 KiB
Markdown
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`.
|