Deploying an application to Kubernetes is relatively straightforward. Running that application reliably in a production environment requires much more thought.
After working with CI/CD pipelines and Infrastructure as Code, I started looking more closely at what happens after an application reaches a Kubernetes cluster.
Creating a Deployment and exposing it through a Service is only the beginning. Production workloads need health checks, resource controls, scaling, configuration management, controlled updates and a clear recovery strategy.
In this article, I will explore some of the Kubernetes patterns I use to move from a basic deployment towards a more production-ready application architecture.
Production Goal
Application
↓
Kubernetes Deployment
↓
Health Checks + Resource Controls + Scaling
↓
Controlled Rollout
↓
Reliable Production Service
Starting with a Basic Kubernetes Deployment
A simple Kubernetes Deployment may look like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloud-native-api
spec:
replicas: 2
selector:
matchLabels:
app: cloud-native-api
template:
metadata:
labels:
app: cloud-native-api
spec:
containers:
- name: cloud-native-api
image: myregistry/cloud-native-api:1.0
ports:
- containerPort: 8080
This runs two replicas of the application.
For a development environment, this may be enough to get started. In production, however, Kubernetes needs more information about how the application behaves.
Adding Readiness and Liveness Probes
One of the first improvements I make is adding health probes.
Kubernetes should understand the difference between an application that is running and an application that is ready to receive traffic.
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
Readiness Probe
A readiness probe determines whether the application should receive traffic.
A pod may be running but still loading configuration, connecting to dependencies or preparing internal services.
Until the readiness check succeeds, Kubernetes can keep that pod out of Service endpoints.
Liveness Probe
A liveness probe is used to determine whether the application is still functioning.
If the application becomes unhealthy for long enough, Kubernetes can restart the container.
Important: readiness and liveness checks should represent different conditions where possible. A temporary dependency issue should not automatically cause the application container to be restarted unnecessarily.
Using Startup Probes for Slow Starting Applications
Some applications need more time during startup.
If a liveness probe begins too early, Kubernetes may restart the container before the application has completed its startup process.
A startup probe can be used for this scenario.
startupProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 5
failureThreshold: 30
In this example, Kubernetes allows the application additional time to start before liveness checking becomes active.
Defining Resource Requests and Limits
Another important production consideration is resource management.
Without requests and limits, Kubernetes has less information about how much CPU and memory an application requires.
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Resource Requests
Requests help the Kubernetes scheduler understand the minimum resources that should be available for the container.
Resource Limits
Limits define the maximum resource usage allowed for the container.
These values should not be guessed permanently. They should be adjusted using monitoring and real workload behaviour.
My approach: I prefer starting with measured values from testing or observability data and then adjusting resource requests and limits based on how the application actually behaves.
Managing Application Configuration
Application configuration should normally be separated from the container image.
Kubernetes ConfigMaps can be used for non-sensitive configuration.
apiVersion: v1
kind: ConfigMap
metadata:
name: cloud-native-api-config
data:
ASPNETCORE_ENVIRONMENT: "Production"
LOG_LEVEL: "Information"
The configuration can then be injected into the container:
envFrom:
- configMapRef:
name: cloud-native-api-config
This makes it possible to change environment configuration without creating a completely new application image for every setting.
Handling Sensitive Configuration
Sensitive values such as passwords, tokens and certificates should not be stored directly inside application manifests committed to source control.
Kubernetes Secrets can provide a basic mechanism for injecting sensitive values.
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: database-secret
key: password
In larger environments, I would normally integrate Kubernetes with a dedicated secrets management platform rather than relying only on manually created Kubernetes Secrets.
Access controls around secrets are just as important as the mechanism used to store them.
Using Rolling Updates
Kubernetes Deployments support rolling updates.
Instead of stopping every existing pod and then starting the new version, Kubernetes can gradually replace old replicas.
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
With this configuration, Kubernetes can create an additional pod before removing an existing one.
Version 1 Pods
↓
Start Version 2 Pod
↓
Readiness Check Passes
↓
Remove Version 1 Pod
↓
Continue Until Rollout Completes
Readiness probes become especially important during rolling deployments because Kubernetes should only route traffic to new pods once they are actually ready.
Scaling Applications Horizontally
Applications do not always receive the same level of traffic.
Kubernetes can increase or decrease the number of replicas based on resource utilisation or other metrics.
A simple HorizontalPodAutoscaler could look like this:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: cloud-native-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: cloud-native-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
In this example, Kubernetes can scale the Deployment between two and ten replicas.
CPU utilisation is used as the scaling signal.
Normal Traffic
↓
2 Pods
↓
Increased Load
↓
CPU Threshold Reached
↓
Kubernetes Adds More Pods
In more advanced environments, scaling can also be based on application metrics, queue depth or other workload-specific signals.
Distributing Pods Across Nodes
Running multiple replicas does not automatically guarantee high availability if every pod ends up on the same Kubernetes node.
Pod anti-affinity or topology spread constraints can help distribute replicas across different nodes or zones.
A simple topology spread configuration might look like this:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: cloud-native-api
This encourages Kubernetes to distribute matching pods across different worker nodes.
Kubernetes Cluster
↓
Node 1 → API Pod
Node 2 → API Pod
Node 3 → Additional Capacity
Using a Pod Disruption Budget
Maintenance operations can sometimes cause multiple pods to become unavailable.
A PodDisruptionBudget helps define how much voluntary disruption an application can tolerate.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: cloud-native-api-pdb
spec:
minAvailable: 1
selector:
matchLabels:
app: cloud-native-api
This example requests that at least one matching pod remains available during voluntary disruptions.
Graceful Application Shutdown
During a deployment or scaling event, Kubernetes may terminate a pod.
Applications should be given enough time to complete active requests before shutting down.
terminationGracePeriodSeconds: 30
The application should also respond correctly to termination signals so that it can stop accepting new work and complete existing requests where possible.
This becomes particularly important for APIs that process longer-running operations.
Using Immutable Image Versions
I avoid relying exclusively on the latest tag for production deployments.
Instead of:
myregistry/cloud-native-api:latest
I prefer versioned images such as:
myregistry/cloud-native-api:1.0.0
myregistry/cloud-native-api:1.0.1
myregistry/cloud-native-api:1.1.0
Another option is using Git commit identifiers.
myregistry/cloud-native-api:a53fd21
This makes it easier to identify exactly which application build is running and simplifies rollback.
Rolling Back a Failed Deployment
Even a well-tested release can fail after deployment.
Kubernetes keeps Deployment rollout history that can help with recovery.
kubectl rollout history deployment/cloud-native-api
A previous revision can be restored using:
kubectl rollout undo deployment/cloud-native-api
I prefer rollback procedures to be documented and tested before a production incident happens.
Putting Everything Together
A more production-focused Kubernetes Deployment can combine several of these patterns.
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloud-native-api
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: cloud-native-api
template:
metadata:
labels:
app: cloud-native-api
spec:
terminationGracePeriodSeconds: 30
containers:
- name: cloud-native-api
image: myregistry/cloud-native-api:1.0.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: cloud-native-api-config
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
startupProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 5
failureThreshold: 30
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: cloud-native-api
This is still not every production concern, but it provides a much stronger foundation than a basic Kubernetes Deployment.
Production Architecture
Users
↓
Kubernetes Service
↓
Deployment
↓
Readiness + Liveness + Startup Checks
↓
Pod 1 Pod 2 Pod 3
↓
Resource Management + Autoscaling
↓
Controlled Rolling Updates
↓
Monitoring and Recovery
What I Learned From Production Kubernetes
One of the biggest lessons for me was that Kubernetes does not automatically make an application production-ready.
Kubernetes provides the platform and mechanisms, but engineers still need to define how the application should behave.
That includes decisions around:
Application health
Resource requirements
Scaling
Configuration
Secrets
Pod distribution
Deployment strategy
Graceful shutdown
Rollback and recovery
The more clearly these behaviours are defined, the more Kubernetes can help operate the application reliably.
Conclusion
In this article, I moved beyond a basic Kubernetes Deployment and explored patterns that make applications more suitable for production environments.
We covered:
Readiness probes
Liveness probes
Startup probes
CPU and memory requests
Resource limits
ConfigMaps
Secret handling
Rolling updates
Horizontal scaling
Pod distribution
Pod disruption budgets
Graceful shutdown
Immutable image versions
Rollback strategies
For me, this was an important step in understanding cloud-native engineering. Deploying containers is only one part of the problem.
The bigger challenge is designing applications and infrastructure so that failures, scaling events and deployments can happen without creating unnecessary disruption.
My next step: After building a stronger Kubernetes foundation, I started looking at how automation could help me reduce repetitive operational tasks. In the next article, I will explore how I use Python to build a practical infrastructure health checker for DevOps environments.