Files
project-infra/docs/examples/infra/workload-selection.md
T

992 lines
32 KiB
Markdown

# 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: <short>` + 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 가능. 해결: 항상 명시.