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

12 KiB

architecture / environments 예시

이 파일의 모든 YAML은 kubectl apply --server-side --dry-run=server 에 통과해야 한다. 모든 예시는 1000+ 서비스 운영 기준으로 작성되었고, 단독으로 복붙해서 바로 apply 할 수 있도록 self-contained 하다.


좋은 예시 1: namespace에 환경 · 도메인 · PodSecurity · 운영 label 전부 박기

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)

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

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 레벨에서 선언

# /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 패턴

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

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에 없음)

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/ 디렉터리에 운영 리소스 직접 배치

/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 포함

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가 공유

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 커스터마이즈

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 사용.