Steven's Knowledge

Best Practices

Production-ready Kubernetes - RBAC, security, GitOps, manifest management, and operational habits

Best Practices

The features in earlier pages let you run a cluster. This page is about running one safely and repeatably at scale.

Manifest Management

Hand-written YAML doesn't scale past one environment. Pick a tool:

ToolApproachBest for
KustomizePatch a base for each envBuilt into kubectl (-k); minimal cognitive load
HelmTemplated charts + valuesReusable packages, community charts
CDK8s / PulumiReal programming languageStrong typing; complex logic
JsonnetFunctional templatingLarge orgs with shared standards

The Kustomize pattern:

manifests/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
└── overlays/
    ├── staging/
    │   ├── kustomization.yaml
    │   └── replicas.yaml
    └── production/
        ├── kustomization.yaml
        └── replicas.yaml
kubectl apply -k manifests/overlays/production

GitOps

Stop running kubectl apply from laptops. Git is the source of truth; an operator reconciles the cluster to match.

ToolNotes
ArgoCDUI-driven, sync on schedule or push, Application CRDs
FluxCLI-first, GitOps Toolkit, pull-based reconciliation

A typical setup:

  • One git repo (or directory) per environment.
  • Argo/Flux watches it; any commit becomes a cluster change.
  • Changes go through PR review.
  • Out-of-band cluster edits get auto-corrected (or trigger an alert).
# ArgoCD Application — points the cluster at a git directory
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-server-production
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/infra.git
    targetRevision: main
    path: manifests/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

RBAC: Least Privilege

Every API call is authenticated and authorized. Don't use cluster-admin for normal work.

# Read-only access to one namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: viewer
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: alice-viewer
  namespace: production
subjects:
  - kind: User
    name: alice@example.com
roleRef:
  kind: Role
  name: viewer
  apiGroup: rbac.authorization.k8s.io

Workload identity

Pods that talk to the K8s API need a ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: cert-renewer
  namespace: production
---
# Bind it to a Role
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: cert-renewer
  namespace: production
subjects:
  - kind: ServiceAccount
    name: cert-renewer
    namespace: production
roleRef:
  kind: Role
  name: secret-writer
  apiGroup: rbac.authorization.k8s.io

For cloud APIs (AWS, GCP, Azure), use Workload Identity — your cloud provider trusts a specific ServiceAccount and issues short-lived credentials. No more long-lived keys in Secrets.

Pod Security

spec:
  securityContext:
    runAsNonRoot: true                     # refuse to start as root
    runAsUser: 1000
    fsGroup: 2000
    seccompProfile: { type: RuntimeDefault }
  containers:
    - name: app
      image: myregistry/app:v1.2.3
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true       # writes go to mounted volumes only
        capabilities:
          drop: ["ALL"]
      resources:                           # ALWAYS set these
        requests: { cpu: "250m", memory: "256Mi" }
        limits:   { cpu: "1000m", memory: "512Mi" }

Enforce these at admission with Pod Security Admission (built-in) or Kyverno / OPA Gatekeeper for richer policies.

Image Hygiene

HabitWhy
Pin tags, never latestRolling updates need to know "what's new"
Use digests in production (@sha256:...)The same tag can point to different images over time
Scan images in CI (Trivy, Grype)Block known CVEs before they ship
Sign images (Cosign / Sigstore)Verify provenance at admission
Distroless / minimal baseSmaller blast radius, fewer CVEs
Private registry with cachingFaster pulls, less Docker Hub rate-limiting

Observability

You can't operate what you can't see. Minimum:

  • Logs — ship from every pod (Fluent Bit DaemonSet → Loki / Elasticsearch / a SaaS).
  • Metrics — see Prometheus & Grafana.
  • Traces — OpenTelemetry SDK in apps, OTel Collector in cluster, ship to Jaeger / Tempo / a SaaS.
  • Eventskubectl get events is useful but ephemeral; ship them too.

Resource Hygiene

PracticeWhy
Resource requests on every containerWithout them, the scheduler packs blindly; HPA breaks
Resource limits on every containerWithout them, one bad pod starves the node
Don't overcommit memoryOOMKills are unrecoverable; CPU throttling is recoverable
LimitRange per namespaceCatch missing requests/limits at admission
ResourceQuota per namespaceCaps total CPU/memory/storage for one team
Cluster AutoscalerAdd/remove nodes based on pending pods
# Namespace-wide caps
apiVersion: v1
kind: ResourceQuota
metadata:
  name: production-quota
  namespace: production
spec:
  hard:
    requests.cpu: "100"
    requests.memory: "200Gi"
    limits.cpu: "200"
    limits.memory: "400Gi"
    pods: "200"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: production-defaults
  namespace: production
spec:
  limits:
    - type: Container
      defaultRequest: { cpu: "100m", memory: "128Mi" }
      default:        { cpu: "500m", memory: "512Mi" }

Multi-tenancy

For shared clusters (multiple teams, multiple environments):

  • One namespace per app + env at minimum.
  • NetworkPolicies default-deny + explicit allowlists.
  • ResourceQuota + LimitRange per namespace.
  • RBAC: humans get Roles in their namespaces, not ClusterRoles.
  • PriorityClass for critical workloads so they survive eviction.

Operational Habits

A handful that pay off:

  1. Never edit live resources. Always change git → reconcile. Drift is debt.
  2. One thing per PR. Replicas, env, image — separate changes, separate rollouts.
  3. Test deploys on staging first. Same Kustomize/Helm overlay, different values.
  4. Practice rollbacks. kubectl rollout undo should be muscle memory.
  5. Capacity-plan with kubectl top and HPA history, not gut feel.
  6. Run game days. Kill a pod, drain a node, fail a region. See what breaks.
  7. Pin and upgrade. K8s versions, controller versions, Helm chart versions.
  8. Don't run databases on K8s unless someone owns it. Managed RDS/Cloud SQL is almost always the right call.

Checklist

Pre-production Kubernetes checklist

  • Manifests version-controlled and templated (Kustomize / Helm)
  • GitOps controller (ArgoCD / Flux) reconciling the cluster
  • Per-app namespaces with NetworkPolicy default-deny
  • requests and limits on every container
  • Readiness + liveness probes on every container
  • PodDisruptionBudget on every important Deployment
  • HPA configured for traffic-driven services
  • Cluster Autoscaler installed
  • RBAC: humans use Roles, ServiceAccounts use Workload Identity
  • Pod Security Admission set to restricted (or Kyverno policies)
  • Image scanning in CI; signed images enforced at admission
  • Logs, metrics, traces shipped off-cluster
  • Backup strategy for stateful workloads (Velero, snapshots)
  • Documented runbook for "node down", "image won't pull", "DB unreachable"

On this page