# operations / runbook / upgrade / rollback 예시 --- ## 좋은 예시 1: 표준 application 변경 절차 ```bash # 1) render kubectl kustomize gitops/clusters/prod/main/stages/50-apps > /tmp/render.yaml # 2) diff kubectl diff -k gitops/clusters/prod/main/stages/50-apps # 3) apply kubectl apply -k gitops/clusters/prod/main/stages/50-apps # 4) rollout status with timeout kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m # 5) smoke test curl -fsS https://auth.internal.example.com/actuator/health/readiness # 6) SLO dashboard check (p99 latency, error rate) ``` **왜 좋은가:** - render / diff / apply / status / post-check 가 명시적으로 분리. - `--timeout` 으로 무한 대기 방지. - post-check가 단순 curl 이 아니라 readiness endpoint 대상. --- ## 좋은 예시 2: Deployment rollingUpdate 파라미터 워크로드별 튜닝 ```yaml --- apiVersion: apps/v1 kind: Deployment metadata: name: auth-server namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod spec: replicas: 10 strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 10% # latency-sensitive면 0 selector: matchLabels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod template: metadata: labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: auth-server image: registry.example.com/identity/auth-server:1.25.0 ports: - { name: http, containerPort: 8080 } resources: requests: { cpu: 500m, memory: 1Gi } limits: { memory: 1536Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: { drop: ["ALL"] } --- apiVersion: apps/v1 kind: Deployment metadata: name: legacy-singleton namespace: legacy spec: replicas: 1 strategy: type: Recreate # singleton이며 동시성 금지 selector: matchLabels: app.kubernetes.io/name: legacy-singleton template: metadata: labels: app.kubernetes.io/name: legacy-singleton spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: app image: registry.example.com/legacy/singleton:1.0.0 resources: requests: { cpu: 100m, memory: 256Mi } limits: { memory: 512Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: { drop: ["ALL"] } ``` **왜 좋은가:** - fleet 규모에 맞춘 maxSurge/maxUnavailable. - singleton 에 Recreate (PVC ReadWriteOnce 전제 충족). --- ## 좋은 예시 3: Argo Rollouts canary with AnalysisTemplate ```yaml --- apiVersion: argoproj.io/v1alpha1 kind: AnalysisTemplate metadata: name: success-rate namespace: auth-prod spec: args: - name: service-name metrics: - name: success-rate interval: 1m count: 5 successCondition: result[0] >= 0.99 failureLimit: 2 provider: prometheus: address: http://prometheus.monitoring.svc:9090 query: | sum(rate(http_requests_total{service="{{args.service-name}}",status_class=~"2.."}[2m])) / sum(rate(http_requests_total{service="{{args.service-name}}"}[2m])) - name: p99-latency interval: 1m count: 5 successCondition: result[0] <= 0.5 failureLimit: 2 provider: prometheus: address: http://prometheus.monitoring.svc:9090 query: | histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{service="{{args.service-name}}"}[2m])) ) --- apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: auth-server namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod spec: replicas: 10 revisionHistoryLimit: 5 selector: matchLabels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod template: metadata: labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod spec: containers: - name: auth-server image: registry.example.com/identity/auth-server:1.25.0 ports: - name: http containerPort: 8080 resources: requests: cpu: "500m" memory: "1Gi" limits: memory: "1536Mi" readinessProbe: httpGet: path: /actuator/health/readiness port: http strategy: canary: canaryService: auth-server-canary stableService: auth-server-stable trafficRouting: nginx: stableIngress: auth-server steps: - setWeight: 10 - pause: { duration: 2m } - analysis: templates: - templateName: success-rate args: - name: service-name value: auth-server - setWeight: 25 - pause: { duration: 5m } - analysis: templates: - templateName: success-rate args: - name: service-name value: auth-server - setWeight: 50 - pause: { duration: 10m } - analysis: templates: - templateName: success-rate args: - name: service-name value: auth-server - setWeight: 100 --- apiVersion: v1 kind: Service metadata: name: auth-server-stable namespace: auth-prod spec: selector: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod ports: - name: http port: 80 targetPort: http --- apiVersion: v1 kind: Service metadata: name: auth-server-canary namespace: auth-prod spec: selector: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod ports: - name: http port: 80 targetPort: http ``` **왜 좋은가:** - `AnalysisTemplate` 이 Prometheus success-rate + p99 latency 를 동시에 측정. - `failureLimit: 2` → 두 번 실패 시 자동 abort. - canary step: 10% → 25% → 50% → 100% 각 단계에 pause + analysis. - stable/canary Service 두 개 + NGINX ingress traffic routing. --- ## 좋은 예시 4: K3s System Upgrade Controller Plan (server + agent) ```yaml --- apiVersion: v1 kind: Namespace metadata: name: system-upgrade --- apiVersion: v1 kind: Secret metadata: name: k3s-upgrade-token namespace: system-upgrade type: Opaque stringData: # 실제 환경은 K3S_TOKEN 값 token: "REPLACE_WITH_NODE_TOKEN" --- apiVersion: upgrade.cattle.io/v1 kind: Plan metadata: name: k3s-server namespace: system-upgrade labels: k3s-upgrade: server spec: concurrency: 1 nodeSelector: matchExpressions: - { key: node-role.kubernetes.io/control-plane, operator: In, values: ["true"] } serviceAccountName: system-upgrade cordon: true drain: force: true deleteEmptydirData: true ignoreDaemonSets: true skipWaitForDeleteTimeout: 60 upgrade: image: rancher/k3s-upgrade version: v1.30.3+k3s1 --- apiVersion: upgrade.cattle.io/v1 kind: Plan metadata: name: k3s-agent namespace: system-upgrade labels: k3s-upgrade: agent spec: concurrency: 1 nodeSelector: matchExpressions: - { key: node-role.kubernetes.io/control-plane, operator: NotIn, values: ["true"] } serviceAccountName: system-upgrade prepare: image: rancher/k3s-upgrade args: ["prepare", "k3s-server"] # server plan 완료 대기 cordon: true drain: force: true deleteEmptydirData: true ignoreDaemonSets: true skipWaitForDeleteTimeout: 60 upgrade: image: rancher/k3s-upgrade version: v1.30.3+k3s1 ``` **왜 좋은가:** - server-plan → agent-plan 분리 + agent 가 `prepare` 로 server 완료 대기. - `concurrency: 1` → 한 번에 한 노드만 업그레이드 (가용성 보호). - `cordon + drain` → PDB 존중. - `deleteEmptydirData: true, ignoreDaemonsets: true` 표준. - `version` 명시 (channel 사용 시 의도치 않은 upgrade 가능). --- ## 좋은 예시 5: ArgoCD sync wave + PreSync migration hook ```yaml --- apiVersion: batch/v1 kind: Job metadata: name: flyway-migrate namespace: auth-prod annotations: argocd.argoproj.io/hook: PreSync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation argocd.argoproj.io/sync-wave: "-1" labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod app.kubernetes.io/component: db-migration spec: backoffLimit: 0 activeDeadlineSeconds: 600 ttlSecondsAfterFinished: 86400 template: spec: restartPolicy: Never securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: flyway image: flyway/flyway:10.15.0 args: ["-url=jdbc:postgresql://postgres:5432/auth", "validate", "info", "migrate"] envFrom: - secretRef: name: auth-db resources: requests: { cpu: 100m, memory: 128Mi } limits: { memory: 512Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] volumeMounts: - { name: tmp, mountPath: /tmp } volumes: - name: tmp emptyDir: {} --- apiVersion: apps/v1 kind: Deployment metadata: name: auth-server namespace: auth-prod annotations: argocd.argoproj.io/sync-wave: "0" labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod # ... (생략) --- apiVersion: batch/v1 kind: Job metadata: name: smoke-test namespace: auth-prod annotations: argocd.argoproj.io/hook: PostSync argocd.argoproj.io/hook-delete-policy: HookSucceeded argocd.argoproj.io/sync-wave: "1" spec: backoffLimit: 2 activeDeadlineSeconds: 300 ttlSecondsAfterFinished: 3600 template: spec: restartPolicy: OnFailure securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: smoke image: registry.example.com/tools/smoke:1.4.0 args: ["--target", "https://auth.internal.example.com"] resources: requests: { cpu: 50m, memory: 64Mi } limits: { memory: 128Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] ``` **왜 좋은가:** - PreSync Job 으로 Flyway migrate 가 app rollout 앞 단계에 실행. - PostSync Job 으로 smoke test 자동 실행. - sync-wave 로 순서 명시 (-1 → 0 → 1). - `BeforeHookCreation` 으로 이전 Job 충돌 방지. --- ## 좋은 예시 6: blue/green via two Services (수동 패턴) ```yaml --- apiVersion: v1 kind: Service metadata: name: auth-server # live traffic namespace: auth-prod spec: selector: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod version: blue # <- 이 label만 바꾸면 cutover ports: - name: http port: 80 targetPort: http --- apiVersion: apps/v1 kind: Deployment metadata: name: auth-server-blue namespace: auth-prod spec: replicas: 3 selector: matchLabels: app.kubernetes.io/name: auth-server version: blue template: metadata: labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod version: blue spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: auth-server image: registry.example.com/identity/auth-server:1.24.0 resources: requests: { cpu: 500m, memory: 1Gi } limits: { memory: 1536Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] --- apiVersion: apps/v1 kind: Deployment metadata: name: auth-server-green namespace: auth-prod spec: replicas: 3 selector: matchLabels: app.kubernetes.io/name: auth-server version: green template: metadata: labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod version: green spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: auth-server image: registry.example.com/identity/auth-server:1.25.0 resources: requests: { cpu: 500m, memory: 1Gi } limits: { memory: 1536Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] ``` Cutover: ```bash kubectl patch svc auth-server -n auth-prod \ -p '{"spec":{"selector":{"app.kubernetes.io/name":"auth-server","app.kubernetes.io/instance":"auth-server-prod","version":"green"}}}' ``` **왜 좋은가:** - Service selector version label 하나로 전환 / rollback. - canary 가 아니라 instant cutover. - 데이터 호환성이 깨진 경우만 사용. --- ## 좋은 예시 7: node maintenance flow ```bash NODE=worker-3 # 1) cordon kubectl cordon "${NODE}" # 2) drain (PDB 존중) kubectl drain "${NODE}" \ --ignore-daemonsets \ --delete-emptydir-data \ --grace-period=30 \ --timeout=10m # 3) 작업 수행 (OS patch, reboot, ...) # 4) 복귀 kubectl uncordon "${NODE}" # 5) 재배치 확인 kubectl get pods -A -o wide --field-selector spec.nodeName="${NODE}" ``` **왜 좋은가:** - cordon → drain → uncordon 표준 시퀀스. - PDB 위반 시 drain 이 대기, `--timeout=10m` 로 무한 대기 방지. - 플래그 조합이 표준. --- ## 좋은 예시 8: Git revision rollback ```bash # 1) 이전 release tag 체크아웃 git checkout v1.24.0 # 2) diff kubectl diff -k gitops/clusters/prod/main/stages/50-apps # 3) apply kubectl apply -k gitops/clusters/prod/main/stages/50-apps # 4) rollout status kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m ``` **왜 좋은가:** - live-cluster 수정이 아니라 declarative source of truth 기준. - 재현 가능. - `kubectl rollout undo` 대비 audit trail 이 명확 (Git commit 기반). --- ## 나쁜 예시 1: diff 없이 apply ```bash kubectl apply -k gitops/clusters/prod/main/stages/50-apps ``` **문제:** - 실제 변경 범위를 모른 채 적용. - review / 승인 / 검증 프로세스 약화. - 의도치 않은 리소스 삭제/수정 가능 (특히 pruned resource). **Fix:** `kubectl diff -k` 선행. --- ## 나쁜 예시 2: migration을 app startup에 숨김 ```yaml # Deployment container command: ["/bin/sh", "-c", "flyway migrate && java -jar app.jar"] ``` **문제:** - app rollout 실패와 schema 변경 실패가 섞임. - rollout 중 여러 replica 가 동시에 migrate → race condition / lock contention. - 롤백 시 schema 변경이 남음. **Fix:** PreSync Job 또는 별도 CI 단계로 Flyway migrate 를 분리. --- ## 나쁜 예시 3: rollout undo 로 DB rollback 기대 ```bash kubectl rollout undo deployment/auth-server # ... 이제 DB schema 도 되돌아갔을 것이다? ``` **문제:** - rollout undo 는 workload pod template 만 되돌린다. - schema 변경은 남아 있음 → 이전 버전 app이 새 schema 와 mismatch → 500 error. - **rollback ≠ DB rollback**. **Fix:** schema 는 expand/contract 패턴으로 forward-compatible. 이전 버전 코드가 새 schema 에서도 동작하도록 릴리스를 분리. --- ## 나쁜 예시 4: 운영 노드 manifests 디렉터리 직접 편집 ```bash ssh k3s-server-1 vim /var/lib/rancher/k3s/server/manifests/auth-server.yaml ``` **문제:** - Git source of truth 우회. - 멀티 서버 간 동기화 없음. - packaged AddOn 동작과 충돌 가능. - ArgoCD 가 drift 로 인식하고 되돌릴 수 있음. **Fix:** Git PR → render → diff → apply 흐름. --- ## 나쁜 예시 5: Recreate strategy 를 stateless app에 사용 ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: auth-server spec: replicas: 5 strategy: type: Recreate # BAD - stateless 인데 downtime 발생 ``` **문제:** - 모든 replica 동시 종료 → full downtime. - rolling update 의 장점 (점진 전환, rollback 용이) 상실. **Fix:** stateless app은 `RollingUpdate` + 워크로드별 maxSurge/maxUnavailable 튜닝. --- ## 나쁜 예시 6: PDB 없이 drain ```bash kubectl drain worker-3 --ignore-daemonsets --delete-emptydir-data ``` **문제:** - PDB 가 없으면 critical workload 가 동시에 evict → downtime. - 특히 replica < 3 이면 완전 손실. **Fix:** PDB 설계 선결 조건. 좋은 예시 7 참조. --- ## 나쁜 예시 7: `kubectl rollout status` 에 timeout 없음 ```bash kubectl rollout status deployment/auth-server -n auth-prod # 무한 대기 가능 ``` **문제:** - rollout 이 hang 상태일 때 CI/CD pipeline 이 무한 대기. - 자동화 실패 원인이 숨는다. **Fix:** 항상 `--timeout=10m` (워크로드별 조정).