Kubernetes — Production Manifest Şablonları#
Bir workload'ı prod'a hazır hale getiren tam set. Güvenlik ayrı bir dosya değil; her manifest içine örülü (non-root, default-deny, least-privilege). Placeholder'lar
<UPPER_CASE>ile. Bu sayfa komşu dosyaların gömülü halidir; kaynak dosyalar aynı klasörde.
Dosyalar#
| Dosya | Kind | Ne sağlar |
|---|---|---|
deployment.yaml | Deployment | Probe, resource, non-root, topology spread |
service.yaml | Service | ClusterIP, http + metrics port |
hpa.yaml | HorizontalPodAutoscaler | CPU/memory hedefli, oscillation-önleyici davranış |
ingress.yaml | Ingress | TLS, rate limit, security header'lar |
networkpolicy.yaml | NetworkPolicy | Default-deny + explicit allow |
pdb.yaml | PodDisruptionBudget | Voluntary disruption'da minimum ayakta pod |
serviceaccount-rbac.yaml | SA + Role + RoleBinding | Least-privilege API erişimi |
Derinlik için: 05-Kubernetes/ ve 08-Security/Kubernetes-Hardening.md.
1️⃣ deployment.yaml#
Probe üçlüsü (startup/liveness/readiness), resource request/limit, non-root + read-only root FS + dropped capabilities, ve HA için topology spread.
# Production-grade Deployment template
# Yer tutucuları (<UPPER_CASE>) kendi değerlerinle değiştir.
# Bu template aşağıdaki en iyi pratikleri içerir:
# - resource requests/limits
# - readiness/liveness/startup probes
# - non-root, read-only root FS, dropped capabilities
# - topology spread (HA için)
# - PodDisruptionBudget örneği (ayrı dosyada — pdb.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
labels:
app.kubernetes.io/name: <APP_NAME>
app.kubernetes.io/component: api
app.kubernetes.io/part-of: <SYSTEM_NAME>
app.kubernetes.io/managed-by: argocd
spec:
replicas: 3
revisionHistoryLimit: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0 # zero-downtime için 0
selector:
matchLabels:
app.kubernetes.io/name: <APP_NAME>
template:
metadata:
labels:
app.kubernetes.io/name: <APP_NAME>
app.kubernetes.io/component: api
annotations:
# Prometheus scrape (eğer kullanıyorsan)
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: <APP_NAME> # least-privilege sa
automountServiceAccountToken: false # gerekmiyorsa kapat
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: <APP_NAME>
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: <APP_NAME>
containers:
- name: app
image: <REGISTRY>/<IMAGE>:<TAG> # SHA-pinned önerilir (sha256:...)
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: APP_VERSION
value: "<VERSION>" # release-please/Argo Image Updater set eder
envFrom:
- configMapRef:
name: <APP_NAME>-config
- secretRef:
name: <APP_NAME>-secrets # External Secrets Operator ile yönetilir
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m # CPU limit'i opsiyonel — throttling yapar
memory: 512Mi # OOM önler
startupProbe:
httpGet:
path: /health/live
port: http
failureThreshold: 30
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 3
lifecycle:
preStop:
exec:
# Graceful shutdown: load balancer'a "kayıyorum" deyince
# mevcut isteklerin bitmesini bekle
command: ["sh", "-c", "sleep 15"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/app
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir:
sizeLimit: 100Mi
2️⃣ service.yaml#
apiVersion: v1
kind: Service
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
labels:
app.kubernetes.io/name: <APP_NAME>
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
- name: metrics
port: 9090
targetPort: metrics
protocol: TCP
selector:
app.kubernetes.io/name: <APP_NAME>
3️⃣ hpa.yaml#
# HorizontalPodAutoscaler — CPU/memory + custom metric
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: <APP_NAME>
minReplicas: 3 # HA için en az 3
maxReplicas: 30
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100 # 60sn'de %100 (2x) artırabilir
periodSeconds: 60
- type: Pods
value: 4 # veya 4 pod ekle
periodSeconds: 60
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300 # 5dk yavaş azalt (oscillation engelle)
policies:
- type: Percent
value: 50
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# Custom metric örneği (Prometheus Adapter kurulu olmalı):
# - type: Pods
# pods:
# metric:
# name: http_requests_per_second
# target:
# type: AverageValue
# averageValue: "100"
4️⃣ ingress.yaml#
# Ingress — TLS terminasyon, rate limit, basic security headers
# (NGINX Ingress Controller varsayar; Gateway API için ayrı template)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# Rate limiting — IP başına dakika içinde 100 istek
nginx.ingress.kubernetes.io/limit-rps: "100"
nginx.ingress.kubernetes.io/limit-connections: "20"
# Body size limiti
nginx.ingress.kubernetes.io/proxy-body-size: "8m"
# Timeout'lar
nginx.ingress.kubernetes.io/proxy-connect-timeout: "5"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
# Security headers
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "X-Frame-Options: DENY";
more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
more_set_headers "Permissions-Policy: geolocation=(), microphone=(), camera=()";
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains; preload";
spec:
ingressClassName: nginx
tls:
- hosts:
- <DOMAIN>
secretName: <APP_NAME>-tls
rules:
- host: <DOMAIN>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <APP_NAME>
port:
number: 80
5️⃣ networkpolicy.yaml — default-deny#
Önce her şeyi kapat, sonra sadece gerekeni aç. DNS'e çıkış zorunlu — unutma.
# NetworkPolicy — default-deny + explicit allow
# CNI'ın NetworkPolicy desteklemesi gerekir (Cilium, Calico, vb.)
# 1) Namespace içinde DEFAULT DENY (her şey kapalı)
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: <NAMESPACE>
spec:
podSelector: {} # tüm pod'lar
policyTypes:
- Ingress
- Egress
# 2) DNS'e (kube-dns) çıkışına izin ver — ZORUNLU
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: <NAMESPACE>
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 3) Uygulamanın spesifik allow'ları
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: <APP_NAME>-allow
namespace: <NAMESPACE>
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: <APP_NAME>
policyTypes:
- Ingress
- Egress
ingress:
# Ingress controller'dan trafik kabul et
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 8080
# Aynı namespace'teki monitoring (prometheus scrape)
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
egress:
# Aynı namespace'teki Postgres'e
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: postgres
ports:
- protocol: TCP
port: 5432
# External API çağrıları (HTTPS)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # internal network'lere açık değil
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
6️⃣ pdb.yaml#
# PodDisruptionBudget — voluntary disruption (node drain, vs) sırasında
# en az kaç pod ayakta kalsın garantisi
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
spec:
minAvailable: 2 # veya: maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: <APP_NAME>
unhealthyPodEvictionPolicy: AlwaysAllow # K8s 1.27+ — unhealthy pod'lar evict edilebilir
7️⃣ serviceaccount-rbac.yaml — least-privilege#
# Least-privilege ServiceAccount + Role + RoleBinding
# Uygulamanız Kubernetes API ile konuşuyorsa kullanılır.
apiVersion: v1
kind: ServiceAccount
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
automountServiceAccountToken: false # podSpec'te ayrıca açacağız
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
rules:
# Sadece kendi namespace'inde, sadece spesifik kaynak ve verb'ler
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# Lease (leader election için)
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: <APP_NAME>
namespace: <NAMESPACE>
subjects:
- kind: ServiceAccount
name: <APP_NAME>
namespace: <NAMESPACE>
roleRef:
kind: Role
name: <APP_NAME>
apiGroup: rbac.authorization.k8s.io
🚫 Anti-Pattern#
| Anti-pattern | Niye kötü | Doğru |
|---|---|---|
| Probe yok | K8s hazır olmayan pod'a trafik yollar; ölü pod'u restart etmez | startup + liveness + readiness üçlüsü |
| Resource limit yok | Bir pod node'u aç bırakır; OOM komşuları vurur | request + limit; memory limit OOM sınırı |
| NetworkPolicy yok | Compromise pod tüm cluster'a yatay hareket eder | Default-deny + explicit allow |
cluster-admin ServiceAccount | Compromise token = tüm cluster | Namespace-scoped Role, sadece gereken verb |
| Root container | Escape = node root | runAsNonRoot + drop: ["ALL"] + read-only FS |
| PDB yok | Node drain tüm replica'yı aynı anda düşürür | minAvailable ile taban garanti |
📋 Checklist#
[ ] Deployment: request + limit set
[ ] Deployment: 3 probe (startup/liveness/readiness) tanımlı
[ ] Pod non-root (runAsNonRoot, drop ALL, read-only root FS)
[ ] NetworkPolicy default-deny + DNS allow uygulandı
[ ] ServiceAccount least-privilege (cluster-admin YOK)
[ ] PDB minAvailable ayarlı
[ ] İmaj SHA-pinned (`:latest` YOK)
"Güvenlik ayrı bir manifest değil — her manifestin içinde bir alan. Sonradan eklenmez, baştan örülür."