# architecture / environments 예시 이 파일의 모든 YAML은 `kubectl apply --server-side --dry-run=server` 에 통과해야 한다. 모든 예시는 1000+ 서비스 운영 기준으로 작성되었고, 단독으로 복붙해서 바로 apply 할 수 있도록 self-contained 하다. --- ## 좋은 예시 1: namespace에 환경 · 도메인 · PodSecurity · 운영 label 전부 박기 ```yaml apiVersion: v1 kind: Namespace metadata: name: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/version: "1.24.3" app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/team: identity-sre example.com/tier: backend example.com/slo-tier: tier-1 example.com/data-classification: confidential example.com/cost-center: cc-1042 pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted annotations: example.com/owner-email: identity-sre@example.com example.com/runbook: https://runbooks.example.com/identity/auth example.com/slo-doc: https://slo.example.com/identity/auth --- apiVersion: v1 kind: ResourceQuota metadata: name: default-quota namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: quota app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod spec: hard: requests.cpu: "20" requests.memory: 40Gi limits.cpu: "40" limits.memory: 80Gi pods: "200" persistentvolumeclaims: "20" --- apiVersion: v1 kind: LimitRange metadata: name: default-limits namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: limits app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod spec: limits: - type: Container default: cpu: "500m" memory: 512Mi defaultRequest: cpu: "100m" memory: 128Mi max: cpu: "4" memory: 8Gi min: cpu: "10m" memory: 32Mi ``` **왜 좋은가:** - `app.kubernetes.io/*` well-known 6종이 모두 있고, 운영 축은 `example.com/*`로 분리되어 selector immutability를 깨지 않는다 - PodSecurity admission이 namespace 레벨에서 `restricted`로 강제 → 이후 Pod spec이 noncompliant면 창조 시점에 거부 - ResourceQuota + LimitRange가 namespace 단위로 고정되어 하나의 서비스가 클러스터를 삼킬 수 없다 - 환경(prod)·도메인(identity)·서비스(auth)가 namespace 이름과 label 양쪽에 드러남 --- ## 좋은 예시 2: multi-region prod overlay 디렉터리 (kr-main + kr-dr) ```text gitops/ apps/ identity-auth/ base/ kustomization.yaml deployment.yaml service.yaml servicemonitor.yaml pdb.yaml hpa.yaml overlays/ dev/ staging/ prod/ kr-main/ kustomization.yaml patches/ kr-dr/ kustomization.yaml patches/ flyway-migrate-identity/ base/ overlays/ platform/ ingress-nginx/ cert-manager/ secret-delivery/ clusters/ dev/main/ staging/main/ prod/kr-main/ prod/kr-dr/ ``` **왜 좋은가:** - 1000+ 서비스 스케일에서 단일 overlay/prod로는 region 차이를 표현할 수 없다. region이 overlay 하위 계층이 되어야 한다 - base는 region·환경을 모른다 (원칙 충족) - DR region은 base의 image pull spec만 mirror로 패치하고 나머지는 공유 --- ## 좋은 예시 3: SLO tier 별 기본 default per namespace ```yaml apiVersion: v1 kind: ConfigMap metadata: name: slo-defaults namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: slo-config app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/slo-tier: tier-1 data: availability-slo: "99.95" rpo-minutes: "5" rto-minutes: "15" backup-interval-minutes: "15" multi-az-required: "true" pdb-min-available-percent: "50" ``` **왜 좋은가:** - SLO/RPO/RTO 숫자가 YAML로 문서화되어 audit 가능 - 같은 tier 정의가 팀마다 제각각 drift 되는 일을 막는다 - `example.com/slo-tier` label이 cluster-wide 쿼리 축 제공 (`kubectl get ns -l example.com/slo-tier=tier-1`) --- ## 좋은 예시 4: K3s packaged component disable을 bootstrap 레벨에서 선언 ```yaml # /etc/rancher/k3s/config.yaml (Git-managed, applied identically to every server node) write-kubeconfig-mode: "0640" cluster-cidr: "10.42.0.0/16" service-cidr: "10.43.0.0/16" cluster-dns: "10.43.0.10" cluster-domain: "cluster.local" disable: - traefik - servicelb - local-storage disable-network-policy: false tls-san: - "k3s.prod.example.internal" - "10.0.0.10" 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" kubelet-arg: - "config=/etc/rancher/k3s/kubelet.yaml" ``` **왜 좋은가:** - prod 스케일에서 traefik / servicelb / local-storage는 전부 외부 컴포넌트로 대체되므로 disable이 기본 - critical config (`cluster-cidr`, `service-cidr`, `cluster-dns`, `cluster-domain`)가 Git 하나의 파일에 고정 → 서버 간 mismatch 불가능 - audit log와 kubelet config가 선언형으로 박힘 → 신규 서버 조인 시 drift 없음 --- ## 좋은 예시 5: 도메인 분리 + public/internal/operator ingress host 패턴 ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: auth-public namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/version: "1.24.3" app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/exposure: public annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: "8m" spec: ingressClassName: nginx-public tls: - hosts: - auth.example.com secretName: auth-public-tls rules: - host: auth.example.com http: paths: - path: / pathType: Prefix backend: service: name: auth port: number: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: auth-admin namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: admin app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/exposure: operator-only annotations: cert-manager.io/cluster-issuer: internal-ca nginx.ingress.kubernetes.io/auth-url: "https://sso.ops.example.com/oauth2/auth" nginx.ingress.kubernetes.io/auth-signin: "https://sso.ops.example.com/oauth2/sign_in?rd=$escaped_request_uri" nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8" spec: ingressClassName: nginx-internal tls: - hosts: - auth.ops.example.com secretName: auth-admin-tls rules: - host: auth.ops.example.com http: paths: - path: /actuator pathType: Prefix backend: service: name: auth port: number: 8081 ``` **왜 좋은가:** - 한 서비스(auth)가 public API와 operator-only admin 포트를 별도 ingress + 별도 ingressClass + 별도 TLS issuer로 분리 - CIDR whitelist + OAuth2 sso forward-auth가 admin endpoint에 강제 - `example.com/exposure` label로 cluster-wide audit 쿼리 가능 --- ## 나쁜 예시 1: `default` namespace에 prod workload ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: auth-server namespace: default spec: replicas: 3 selector: matchLabels: app: auth-server template: metadata: labels: app: auth-server spec: containers: - name: auth image: registry.example.com/auth:1.24.3 ``` **문제:** `default` namespace는 PodSecurity / Quota / NetworkPolicy를 걸기 위한 격리 단위가 될 수 없고, 다른 팀 리소스와 섞인다. 1000-서비스 환경에서 `default`는 영구적으로 비워두는 것이 운영 원칙. --- ## 나쁜 예시 2: `app.kubernetes.io/environment` 사용 (well-known label에 없음) ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: auth namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/environment: prod # invalid well-known key ``` **문제:** Kubernetes 공식 well-known label set은 `{name,instance,version,component,part-of,managed-by}` 6종뿐. `environment`는 여기 없으므로 **자체 도메인**(`example.com/environment`)을 써야 한다. 다른 팀이 `app.kubernetes.io/env` 같은 변종을 만들어 drift가 퍼진다. --- ## 나쁜 예시 3: `manifests/` 디렉터리에 운영 리소스 직접 배치 ```text /var/lib/rancher/k3s/server/manifests/auth-prod.yaml /var/lib/rancher/k3s/server/manifests/keycloak-prod.yaml /var/lib/rancher/k3s/server/manifests/ingress-nginx.yaml ``` **문제:** 멀티 서버 K3s는 이 디렉터리를 서버 간 동기화하지 **않는다**. 서버 A에만 있는 파일은 서버 B 리더가 되면 사라진 것처럼 보인다. source of truth는 Git + Kustomize여야 한다. --- ## 나쁜 예시 4: selector에 버전 / 환경 label 포함 ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: auth namespace: prod-identity-auth spec: selector: matchLabels: app.kubernetes.io/name: auth app.kubernetes.io/version: "1.24.3" # changes on every release example.com/environment: prod # injected by overlay template: metadata: labels: app.kubernetes.io/name: auth app.kubernetes.io/version: "1.24.3" example.com/environment: prod spec: containers: - name: auth image: registry.example.com/auth:1.24.3 ``` **문제:** `selector.matchLabels`는 Deployment/StatefulSet에서 **immutable**이다. `version`은 배포마다 바뀌고 `environment`는 overlay가 주입한다 → 첫 배포 이후 재apply 시 `field is immutable` 에러로 영구 차단. selector에는 불변 2종(`name`/`instance`)만. --- ## 나쁜 예시 5: 같은 hostname을 dev와 prod가 공유 ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: auth namespace: dev-identity-auth spec: ingressClassName: nginx-public rules: - host: auth.example.com # same as prod http: paths: - path: / pathType: Prefix backend: service: name: auth port: number: 8080 ``` **문제:** 환경 간 host 공유는 TLS cert race, 동일 hostname의 두 ingress 간 routing 불확실성, 외부 모니터링이 어느 환경을 보는지 혼동을 유발한다. dev는 반드시 `auth.dev.example.com` 같이 별도 hostname을 쓴다. --- ## 나쁜 예시 6: K3s traefik manifest 직접 수정으로 prod ingress 커스터마이즈 ```bash vim /var/lib/rancher/k3s/server/manifests/traefik.yaml # added custom middleware config inline systemctl restart k3s ``` **문제:** K3s는 재시작 시 이 파일을 packaged 원본으로 overwrite한다. 운영 커스터마이징이 조용히 사라진다. prod 1000-서비스 스케일에서는 `--disable=traefik` 후 ingress-nginx를 별도 컴포넌트로 관리하는 것이 유일한 정답. 유지한다면 **반드시** `HelmChartConfig` 사용.