# workload selection 예시 모든 YAML은 `kubectl apply --server-side --dry-run=server` 통과. PodSecurity `restricted` 호환. 각 예시는 namespace 하나에 그대로 붙여 넣을 수 있는 self-contained 단위. --- ## 좋은 예시 1: auth-server Deployment (tier-1 prod, 완전 동반 리소스) ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: auth namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod automountServiceAccountToken: false --- apiVersion: apps/v1 kind: Deployment metadata: name: auth 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/slo-tier: tier-1 spec: replicas: 6 revisionHistoryLimit: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 0 selector: matchLabels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api template: metadata: labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/slo-tier: tier-1 spec: serviceAccountName: auth automountServiceAccountToken: false priorityClassName: tier-1-critical terminationGracePeriodSeconds: 45 securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api containers: - name: auth image: registry.example.com/auth@sha256:f1a2b3c4d5e6f7081920aabbccddeeff00112233445566778899aabbccddeeff imagePullPolicy: IfNotPresent ports: - { name: http, containerPort: 8080, protocol: TCP } - { name: management, containerPort: 8081, protocol: TCP } resources: requests: { cpu: 500m, memory: 1Gi } limits: { cpu: "2", memory: 2Gi } startupProbe: httpGet: { path: /actuator/health/liveness, port: management } periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: { path: /actuator/health/liveness, port: management } periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: { path: /actuator/health/readiness, port: management } periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] securityContext: runAsNonRoot: true runAsUser: 10001 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ "ALL" ] volumeMounts: - { name: tmp, mountPath: /tmp } volumes: - name: tmp emptyDir: { sizeLimit: 64Mi } --- apiVersion: v1 kind: Service metadata: name: auth namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: type: ClusterIP selector: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api ports: - { name: http, port: 8080, targetPort: http } - { name: management, port: 8081, targetPort: management } --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: auth namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/slo-tier: tier-1 spec: minAvailable: 50% unhealthyPodEvictionPolicy: AlwaysAllow selector: matchLabels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: auth namespace: prod-identity-auth labels: app.kubernetes.io/name: auth app.kubernetes.io/instance: auth-prod app.kubernetes.io/component: api app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: auth minReplicas: 6 maxReplicas: 30 metrics: - type: Resource resource: name: cpu target: { type: Utilization, averageUtilization: 70 } - type: Resource resource: name: memory target: { type: Utilization, averageUtilization: 80 } behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - { type: Percent, value: 25, periodSeconds: 60 } scaleUp: stabilizationWindowSeconds: 0 policies: - { type: Percent, value: 100, periodSeconds: 30 } - { type: Pods, value: 4, periodSeconds: 30 } selectPolicy: Max ``` **왜 좋은가:** - stateless 장기 실행 → Deployment 정답 - PDB + HPA + topologySpread (zone+hostname) 모두 tier-1에 맞게 동반 - digest pinning, restricted PodSecurity 호환, preStop sleep으로 graceful drain - selector에는 불변 2종만 (`name`/`instance`; version/environment 없음) --- ## 좋은 예시 2: PostgreSQL StatefulSet (operator 없는 fallback 케이스, 완전 schema) ```yaml apiVersion: v1 kind: Service metadata: name: postgres-identity-headless namespace: prod-data-postgres labels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: clusterIP: None selector: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database ports: - { name: postgres, port: 5432, targetPort: postgres } --- apiVersion: v1 kind: Service metadata: name: postgres-identity namespace: prod-data-postgres labels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: type: ClusterIP selector: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database ports: - { name: postgres, port: 5432, targetPort: postgres } --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres-identity namespace: prod-data-postgres labels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/version: "16.3" app.kubernetes.io/component: database app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/slo-tier: tier-1 example.com/data-classification: confidential spec: serviceName: postgres-identity-headless replicas: 3 podManagementPolicy: OrderedReady updateStrategy: type: RollingUpdate rollingUpdate: partition: 0 persistentVolumeClaimRetentionPolicy: whenDeleted: Retain whenScaled: Retain revisionHistoryLimit: 5 selector: matchLabels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database template: metadata: labels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: serviceAccountName: postgres-identity automountServiceAccountToken: false priorityClassName: tier-1-critical terminationGracePeriodSeconds: 120 securityContext: runAsNonRoot: true runAsUser: 999 runAsGroup: 999 fsGroup: 999 seccompProfile: type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database containers: - name: postgres image: registry.example.com/postgres@sha256:aabbccddeeff00112233445566778899aabbccddeeff0011223344556677abcd imagePullPolicy: IfNotPresent ports: - { name: postgres, containerPort: 5432, protocol: TCP } env: - name: POSTGRES_DB value: identity - name: POSTGRES_USER valueFrom: secretKeyRef: { name: postgres-identity-creds, key: username } - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: { name: postgres-identity-creds, key: password } - name: PGDATA value: /var/lib/postgresql/data/pgdata resources: requests: { cpu: "2", memory: 4Gi } limits: { cpu: "4", memory: 8Gi } startupProbe: exec: command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"] periodSeconds: 5 failureThreshold: 60 livenessProbe: exec: command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"] periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: exec: command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"] periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 securityContext: runAsNonRoot: true runAsUser: 999 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ "ALL" ] volumeMounts: - { name: data, mountPath: /var/lib/postgresql/data } - { name: tmp, mountPath: /tmp } - { name: run, mountPath: /var/run/postgresql } volumes: - name: tmp emptyDir: {} - name: run emptyDir: {} volumeClaimTemplates: - metadata: name: data labels: app.kubernetes.io/name: postgres app.kubernetes.io/instance: postgres-identity app.kubernetes.io/component: database app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: accessModes: [ "ReadWriteOnce" ] storageClassName: longhorn-replicated resources: requests: storage: 200Gi ``` **왜 좋은가:** - headless Service + clusterIP Service 양쪽 선언 (peer discovery + 일반 client) - `persistentVolumeClaimRetentionPolicy: {whenDeleted: Retain, whenScaled: Retain}` 명시 (GA 1.27) - `podManagementPolicy: OrderedReady` + `updateStrategy.partition: 0` (canary 시 1씩) - zone topologySpread `DoNotSchedule`로 강제 (DB는 AZ 분산이 강건성 핵심) - Secret 외부 참조 (External Secrets로 관리 가정) - StorageClass `longhorn-replicated` (local-path 금지) **실전 주의**: 1000-서비스 스케일에서는 raw StatefulSet 대신 **CloudNativePG Operator** 사용을 강력 권장. 이 예시는 operator 불가 케이스의 reference. --- ## 좋은 예시 3: fluent-bit DaemonSet (로그 shipper) ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: fluent-bit namespace: prod-platform-observability labels: app.kubernetes.io/name: fluent-bit app.kubernetes.io/instance: fluent-bit-prod app.kubernetes.io/component: log-shipper app.kubernetes.io/part-of: observability-platform app.kubernetes.io/managed-by: argocd --- apiVersion: apps/v1 kind: DaemonSet metadata: name: fluent-bit namespace: prod-platform-observability labels: app.kubernetes.io/name: fluent-bit app.kubernetes.io/instance: fluent-bit-prod app.kubernetes.io/version: "3.1.7" app.kubernetes.io/component: log-shipper app.kubernetes.io/part-of: observability-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod spec: selector: matchLabels: app.kubernetes.io/name: fluent-bit app.kubernetes.io/instance: fluent-bit-prod app.kubernetes.io/component: log-shipper updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 10% revisionHistoryLimit: 5 template: metadata: labels: app.kubernetes.io/name: fluent-bit app.kubernetes.io/instance: fluent-bit-prod app.kubernetes.io/component: log-shipper app.kubernetes.io/part-of: observability-platform app.kubernetes.io/managed-by: argocd spec: serviceAccountName: fluent-bit automountServiceAccountToken: true priorityClassName: system-node-critical hostNetwork: false terminationGracePeriodSeconds: 30 tolerations: - operator: Exists securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: fluent-bit image: registry.example.com/fluent-bit@sha256:112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00 imagePullPolicy: IfNotPresent ports: - { name: metrics, containerPort: 2020, protocol: TCP } resources: requests: { cpu: 100m, memory: 128Mi } limits: { cpu: 500m, memory: 256Mi } livenessProbe: httpGet: { path: /, port: metrics } periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: { path: /api/v1/health, port: metrics } periodSeconds: 5 failureThreshold: 3 securityContext: runAsNonRoot: true runAsUser: 10001 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ "ALL" ] add: [ "DAC_READ_SEARCH" ] volumeMounts: - { name: varlog, mountPath: /var/log, readOnly: true } - { name: varlibdockercontainers, mountPath: /var/lib/docker/containers, readOnly: true } - { name: config, mountPath: /fluent-bit/etc } volumes: - name: varlog hostPath: { path: /var/log, type: Directory } - name: varlibdockercontainers hostPath: { path: /var/lib/docker/containers, type: DirectoryOrCreate } - name: config configMap: { name: fluent-bit-config } ``` **왜 좋은가:** - DaemonSet으로 모든 노드에 정확히 1 Pod - `tolerations: Exists`로 control-plane taint 포함 모든 노드 커버 - `priorityClassName: system-node-critical`로 eviction 방지 - root 필요(hostPath 로그 읽기) 하지만 capabilities는 `DAC_READ_SEARCH`만 추가하고 나머지 drop - `updateStrategy.rollingUpdate.maxUnavailable: 10%`로 대규모 클러스터 rolling 안정화 ### ❌ Bad counterpart (같은 역할을 Deployment로) ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: fluent-bit spec: replicas: 10 ``` **문제:** Deployment는 특정 노드에 Pod가 없을 수 있고, 같은 노드에 여러 Pod가 떠서 로그 중복 수집. DaemonSet만 "노드당 정확히 1"을 보장. --- ## 좋은 예시 4: flyway-migrate Job (ArgoCD PostSync hook) ```yaml apiVersion: batch/v1 kind: Job metadata: name: flyway-migrate-identity-1-24-3 namespace: prod-identity-auth labels: app.kubernetes.io/name: flyway app.kubernetes.io/instance: flyway-identity-1-24-3 app.kubernetes.io/version: "1.24.3" app.kubernetes.io/component: schema-migration app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod annotations: argocd.argoproj.io/hook: PostSync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation spec: backoffLimit: 2 activeDeadlineSeconds: 600 ttlSecondsAfterFinished: 86400 template: metadata: labels: app.kubernetes.io/name: flyway app.kubernetes.io/instance: flyway-identity-1-24-3 app.kubernetes.io/component: schema-migration app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: restartPolicy: Never serviceAccountName: flyway-identity automountServiceAccountToken: false priorityClassName: tier-1-critical securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: flyway image: registry.example.com/flyway@sha256:ccddeeff0011223344556677889900aabbccddeeff0011223344556677889900 imagePullPolicy: IfNotPresent args: [ "migrate" ] env: - { name: FLYWAY_URL, valueFrom: { secretKeyRef: { name: flyway-identity-creds, key: url } } } - { name: FLYWAY_USER, valueFrom: { secretKeyRef: { name: flyway-identity-creds, key: username } } } - { name: FLYWAY_PASSWORD, valueFrom: { secretKeyRef: { name: flyway-identity-creds, key: password } } } resources: requests: { cpu: 200m, memory: 256Mi } limits: { cpu: "1", memory: 512Mi } securityContext: runAsNonRoot: true runAsUser: 1000 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ "ALL" ] volumeMounts: - { name: tmp, mountPath: /tmp } volumes: - name: tmp emptyDir: { sizeLimit: 32Mi } ``` **왜 좋은가:** - `restartPolicy: Never` + `backoffLimit: 2` → migration 실패는 debug 가능하게 노출 - `activeDeadlineSeconds: 600` → 무한 lock 방지 - `ttlSecondsAfterFinished: 86400` → 24시간 후 자동 정리 (1000-서비스 스케일 필수) - ArgoCD `PostSync` hook으로 Deployment rollout 이후 실행 - 이름에 버전 suffix (`-1-24-3`) → 같은 이름 Job 재생성 충돌 방지 --- ## 좋은 예시 5: postgres-backup CronJob (timezone + concurrencyPolicy) ```yaml apiVersion: batch/v1 kind: CronJob metadata: name: postgres-identity-backup namespace: prod-data-postgres labels: app.kubernetes.io/name: postgres-backup app.kubernetes.io/instance: postgres-identity-backup app.kubernetes.io/component: backup app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd example.com/environment: prod spec: schedule: "0 */6 * * *" timeZone: "Asia/Seoul" concurrencyPolicy: Forbid startingDeadlineSeconds: 600 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 5 jobTemplate: spec: backoffLimit: 1 activeDeadlineSeconds: 3600 template: metadata: labels: app.kubernetes.io/name: postgres-backup app.kubernetes.io/instance: postgres-identity-backup app.kubernetes.io/component: backup app.kubernetes.io/part-of: identity-platform app.kubernetes.io/managed-by: argocd spec: restartPolicy: Never serviceAccountName: postgres-identity-backup automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 999 runAsGroup: 999 fsGroup: 999 seccompProfile: type: RuntimeDefault containers: - name: pgbackup image: registry.example.com/pgbackup@sha256:ddeeff00112233445566778899aabbccddeeff00112233445566778899aabbcc imagePullPolicy: IfNotPresent env: - { name: PGHOST, value: postgres-identity.prod-data-postgres.svc.cluster.local } - { name: PGUSER, valueFrom: { secretKeyRef: { name: pgbackup-creds, key: username } } } - { name: PGPASSWORD, valueFrom: { secretKeyRef: { name: pgbackup-creds, key: password } } } - { name: S3_BUCKET, value: pg-backups-prod } - { name: S3_PREFIX, value: identity } resources: requests: { cpu: 200m, memory: 256Mi } limits: { cpu: "1", memory: 512Mi } securityContext: runAsNonRoot: true allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ "ALL" ] volumeMounts: - { name: tmp, mountPath: /tmp } volumes: - name: tmp emptyDir: { sizeLimit: 2Gi } ``` **왜 좋은가:** - `timeZone: Asia/Seoul` (v1.25+) → DST 안정 - `concurrencyPolicy: Forbid` → 이전 백업이 돌고 있으면 skip - `startingDeadlineSeconds: 600` → 노드 장애 후 미싱 누적 제한 - history limit으로 완료 Job 정리 (`successfulJobsHistoryLimit: 3`, `failedJobsHistoryLimit: 5`) - CronJob의 jobTemplate에는 `ttlSecondsAfterFinished`를 설정하지 않음 — history limit과 중복/충돌 방지 (standalone Job에서만 사용) **실전 주의**: CloudNativePG `ScheduledBackup` CRD를 쓰면 이 CronJob을 operator가 대체한다. --- ## 좋은 예시 6: Ingress controller Deployment + MetalLB (prod 기본) ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: ingress-nginx-public namespace: prod-platform-ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx-public-prod app.kubernetes.io/version: "1.11.2" app.kubernetes.io/component: controller app.kubernetes.io/part-of: platform app.kubernetes.io/managed-by: argocd example.com/environment: prod example.com/exposure: public example.com/slo-tier: tier-1 spec: replicas: 3 revisionHistoryLimit: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx-public-prod app.kubernetes.io/component: controller template: metadata: labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx-public-prod app.kubernetes.io/component: controller app.kubernetes.io/part-of: platform app.kubernetes.io/managed-by: argocd spec: serviceAccountName: ingress-nginx automountServiceAccountToken: true priorityClassName: system-cluster-critical terminationGracePeriodSeconds: 300 securityContext: runAsNonRoot: true runAsUser: 101 fsGroup: 101 seccompProfile: { type: RuntimeDefault } topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx-public-prod app.kubernetes.io/component: controller containers: - name: controller image: registry.example.com/ingress-nginx@sha256:eeff00112233445566778899aabbccddeeff00112233445566778899aabbccdd args: - /nginx-ingress-controller - --publish-service=$(POD_NAMESPACE)/ingress-nginx-public - --election-id=ingress-nginx-public-leader - --controller-class=k8s.io/ingress-nginx-public - --ingress-class=nginx-public - --configmap=$(POD_NAMESPACE)/ingress-nginx-public env: - { name: POD_NAMESPACE, valueFrom: { fieldRef: { fieldPath: metadata.namespace } } } - { name: POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } } ports: - { name: http, containerPort: 80, protocol: TCP } - { name: https, containerPort: 443, protocol: TCP } - { name: metrics, containerPort: 10254, protocol: TCP } resources: requests: { cpu: 500m, memory: 512Mi } limits: { cpu: "2", memory: 1Gi } livenessProbe: httpGet: { path: /healthz, port: 10254, scheme: HTTP } periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 5 readinessProbe: httpGet: { path: /healthz, port: 10254, scheme: HTTP } periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 lifecycle: preStop: exec: command: ["/wait-shutdown"] securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 101 capabilities: drop: [ "ALL" ] add: [ "NET_BIND_SERVICE" ] volumeMounts: - { name: tmp, mountPath: /tmp } - { name: nginx-etc, mountPath: /etc/nginx } - { name: nginx-cache, mountPath: /var/cache/nginx } volumes: - name: tmp emptyDir: {} - name: nginx-etc emptyDir: {} - name: nginx-cache emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: ingress-nginx-public namespace: prod-platform-ingress-nginx labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx-public-prod app.kubernetes.io/component: controller app.kubernetes.io/part-of: platform app.kubernetes.io/managed-by: argocd annotations: metallb.universe.tf/address-pool: prod-public-pool metallb.universe.tf/allow-shared-ip: "ingress-nginx-public" spec: type: LoadBalancer externalTrafficPolicy: Local selector: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: ingress-nginx-public-prod app.kubernetes.io/component: controller ports: - { name: http, port: 80, targetPort: http, protocol: TCP } - { name: https, port: 443, targetPort: https, protocol: TCP } ``` **왜 좋은가:** - Deployment + LoadBalancer (MetalLB L2/BGP) → HPA 가능, 노드 수 ≠ replica 수 - `externalTrafficPolicy: Local` → source IP 보존 + 노드 horn-in 회피 - `priorityClassName: system-cluster-critical` - `NET_BIND_SERVICE` capability만 추가 (80/443 바인딩), 나머지 drop - public / internal IngressClass 분리 가능 (별도 Deployment) **DaemonSet 선택이 맞는 케이스**: bare-metal + 외부 LB 없음 + 모든 edge 노드가 고정 IP로 80/443 직접 노출. 이 경우 `hostNetwork: true` + DaemonSet + `tolerations`로 edge 노드만 label selector. --- ## 나쁜 예시 1: auth-server를 StatefulSet으로 ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: auth-server spec: serviceName: auth-server 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 ``` **문제:** stable identity/storage 요구가 없는 stateless 앱에 StatefulSet. rolling update가 OrderedReady로 느려지고, replica 증설 시 `auth-server-2`, `auth-server-3` 이름이 의미 없이 고정된다. 해결: Deployment + HPA. --- ## 나쁜 예시 2: postgres를 Deployment로 ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: postgres spec: replicas: 1 template: spec: containers: - name: postgres image: postgres:16 volumeMounts: - { name: data, mountPath: /var/lib/postgresql/data } volumes: - name: data persistentVolumeClaim: { claimName: postgres-pvc } ``` **문제:** replica=1 Deployment + 단일 PVC는 rolling update 시 잠깐 새 Pod가 뜨면서 같은 PVC에 두 Pod가 붙으려다 RWO 충돌. StatefulSet이면 `OrderedReady`로 이전 Pod 완전히 내려간 다음 새 Pod가 뜬다. 해결: StatefulSet + volumeClaimTemplates + podManagementPolicy: OrderedReady. --- ## 나쁜 예시 3: fluent-bit을 Deployment로 배포 ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: fluent-bit spec: replicas: 10 template: spec: containers: - name: fluent-bit image: fluent/fluent-bit:latest volumeMounts: - { name: varlog, mountPath: /var/log } volumes: - name: varlog hostPath: { path: /var/log } ``` **문제:** Deployment는 Pod 배치를 스케줄러에 맡김 → 어떤 노드에는 0 Pod(로그 유실), 어떤 노드에는 2 Pod(중복 수집). 노드 수가 바뀌면 수동으로 replicas 조정. 해결: DaemonSet. --- ## 나쁜 예시 4: flyway를 앱 startup에 포함 ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: auth spec: template: spec: initContainers: - name: flyway-migrate image: registry.example.com/flyway:1.24.3 args: ["migrate"] containers: - name: auth image: registry.example.com/auth:1.24.3 ``` **문제:** Deployment scale-up 때마다 모든 새 Pod가 migration 시도 → DB lock 경합. migration 실패가 앱 부팅 실패로 섞여 debug 불가. rollback 시 downgrade migration 제어 불가. 해결: 독립 Job + ArgoCD PostSync hook. --- ## 나쁜 예시 5: CronJob에 `concurrencyPolicy`와 `startingDeadlineSeconds` 누락 ```yaml apiVersion: batch/v1 kind: CronJob metadata: name: backup spec: schedule: "*/5 * * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: backup image: registry.example.com/backup:1.0.0 ``` **문제:** 1. `concurrencyPolicy` 미지정 → 기본 `Allow` → 이전 backup이 오래 걸리면 중복 실행, PVC lock 충돌 2. `startingDeadlineSeconds` 미지정 → 노드 장애 후 수십 개 missed Job이 한꺼번에 생성 3. history limit 미지정 → 완료 Job이 무한 누적 해결: 모든 prod CronJob에 `concurrencyPolicy: Forbid` + `startingDeadlineSeconds: ` + history limit. --- ## 나쁜 예시 6: StatefulSet에 `persistentVolumeClaimRetentionPolicy` 미지정 (prod) ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: vault spec: serviceName: vault replicas: 3 # persistentVolumeClaimRetentionPolicy missing volumeClaimTemplates: - metadata: { name: data } spec: accessModes: [ "ReadWriteOnce" ] storageClassName: longhorn-replicated resources: requests: { storage: 20Gi } ``` **문제:** 명시가 없으면 기본값 (`{whenDeleted: Retain, whenScaled: Retain}`)이 적용되어 "동작은 맞지만" 의도가 코드에 드러나지 않는다. 팀원이 `{Delete, Delete}`인지 추측. 1000-서비스 스케일에서는 모든 StatefulSet이 이 필드를 **명시**해야 정책이 audit 가능. 해결: 항상 명시.