Files
project-infra/docs/examples/infra/architecture-environments.md
T

418 lines
12 KiB
Markdown

# 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
k8s/
base/
app/
units/
identity/
auth/
kustomization.yaml
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
plugins/
ingress-nginx/
cert-manager/
external-secrets/
managing/
flyway-migrate-identity/
overlays/
dev/
kustomization.yaml
staging/
kustomization.yaml
prod/
kr-main/
kustomization.yaml
patches/
auth-replicas.yaml
auth-resources.yaml
auth-topology-spread.yaml
kr-dr/
kustomization.yaml
patches/
auth-replicas.yaml
auth-image-pull-mirror.yaml
```
**왜 좋은가:**
- 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에는 불변 3종(`name`/`instance`/`component`)만.
---
## 나쁜 예시 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` 사용.