← Back to Blog

Ensuring High Availability for Applications on Kubernetes

We've all heard plenty about the benefits of using Kubernetes to run microservices — which accidentally leads people to believe that just running on Kubernetes automatically gives an app high availability, good fault tolerance, fast scaling, and so on.

In reality, though, achieving High Availability for an application on K8s requires a number of specific configurations tailored to each application. In today's post I'll cover the configurations worth paying attention to if you want your application to run with few errors, no downtime, etc. — collectively, high availability.

Kubernetes high availability overview

Deployment Strategy

Deployment Strategy is the strategy for deploying an application, letting you control how the application is deployed and updated in Kubernetes. There are two common strategies: Rolling Update (Kubernetes' default) and Recreate.

Rolling Update lets you roll out a new version sequentially, updating one pod at a time. This ensures the old version keeps working while the new version is gradually rolled out, causing no downtime.

Recreate, on the other hand, kills all old pods before deploying the new version — suitable for applications that don't need to maintain continuity. This method will cause downtime while waiting for the new version to become ready.

You can also combine canary or blue-green deployment strategies to test a new version safely. Choosing the right deployment strategy helps you keep the service stable during updates and minimize risk throughout the application's lifecycle.

Here's an example config using the Rolling Update strategy for a deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  strategy:
    type: RollingUpdate  # Deployment strategy config
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp-container
        image: myapp-image:v2

Readiness & Liveness

Readiness Probe and Liveness Probe are two important healthchecks for pods in Kubernetes. These two healthchecks help ensure your service always stays ready and stable.

Readiness Probe determines when a container can accept new requests — meaning if a container is loading data or connecting to dependent services, it won't receive traffic until it's fully ready. This prevents users from reaching a service before it's ready to operate, improving the experience. If a user hits a web service while the pod isn't ready, they'll get an HTTP 503 error.

Meanwhile, Liveness Probe checks whether a container is running normally. If a container errors out or falls into an unstable state, Kubernetes restarts the pod to automatically fix it. Combining these two mechanisms improves the system's availability and reliability by ensuring only healthy pods handle requests. How you configure the check timing also matters a lot — I covered best practices for configuring Probes in K8s in an earlier post.

⚠️ Configuring Startup, Readiness, and Liveness Probes for applications running on Kubernetes

Probe config for a Deployment:

apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  containers:
  - name: myapp-container
    image: myapp-image
    readinessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 20

Resource Requests & Limits

Resource Requests and Limits are settings that let you control each container's resource usage, to optimize performance and avoid overloading the system.

Requests are the minimum resources a container needs to run stably, ensuring it always has enough resources when needed.

Limits are the maximum resources a container is allowed to use, preventing a container from hogging all the resources other applications need.

If you don't set these limits, some containers might consume more resources than expected, starving other containers and affecting the stability of the whole cluster. Configuring Requests and Limits sensibly helps you optimize resource distribution in Kubernetes and boost system stability. Setting them too high wastes system resources; setting them too low risks the app getting killed, causing disruption. I've written a whole post on how I choose resource limits and requests for an application — check it out here:

🦾 Kubernetes resource management

Here's a sample resource request and limit config for a K8s deployment:

apiVersion: v1
kind: Pod
metadata:
  name: resource-demo
spec:
  containers:
  - name: resource-demo-container
    image: nginx
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

Horizontal Pod Autoscaler (HPA)

Horizontal Pod Autoscaler (HPA) is a feature that automatically increases or decreases the number of pods for an application based on metrics like CPU or memory. This lets your service automatically scale out to meet high load demand and scale back in when load decreases, saving resources and operating costs. HPA relies on the thresholds you set — for example, when CPU usage hits 70%, HPA automatically adds pods to reduce load on the existing ones. HPA doesn't just help maintain service performance and stability; it's also a handy tool for managing costs in a cloud environment, where resources get allocated based on actual demand.

In most cases you can scale based on CPU, but for some specific applications you need to scale based on memory or the number of incoming requests, which K8s doesn't support natively. In that case, you need additional tools to provide these metrics — Keda is one of them. To learn more about Keda, check out the post I wrote about this tool:

🕵️‍♂️ What is HPA? Auto-scaling pods with HPA and KEDA

Here's a sample HPA config for a K8s application:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

PostStart & PreStop Hook

The PostStart Hook fires right after a container starts, letting you perform any setup needed for the container or prepare the working environment before it officially starts handling requests. For example, you might use PostStart to load necessary config files, establish needed connections, or run authentication and additional configuration steps.

Configuring PostStart or PreStop uses the exec syntax to run commands, or httpGet to call an API right after the container starts.

PostStart doesn't play a huge role in maintaining system availability, but it's a handy feature that can be used in place of an initContainer.

The PreStop Hook is an important hook for making sure a container shuts down smoothly. When a container is about to be terminated, PreStop lets it do some cleanup work or save data. You can use PreStop to close network connections, save data being processed, or notify other services that the container is about to stop. This helps avoid data loss and minimizes impact on the user experience.

In practice, a lot of applications take quite a while to shut down, longer than the default 30 seconds. So DevOps Engineers often configure a sleep 30s command to extend the pod shutdown process, helping the application achieve a graceful shutdown.

Here's a sample config using PostStart and PreStop to stop the nginx service:

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
      preStop:
        exec:
          command: ["/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done"]

Pod Disruption Budget (PDB)

Pod Disruption Budget (PDB) is a mechanism that lets you set the maximum number of pods that can be down at any given time to keep the service running. When you perform a system upgrade or an incident happens, PDB ensures a certain number of pods keep running so the user experience isn't affected. PDB is especially useful combined with update strategies like Rolling Update, letting you perform maintenance without interrupting the service. I consider Pod Disruption Budget a configuration that should always be present in a Production environment.

For example, if you have 5 pods, you can set a PDB to only allow 1 pod down at a time. This ensures that even during an update or an incident, the service always maintains a minimum level of availability.

Sample config for a Pod Disruption Budget with a minimum of 2 available pods:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: myapp

Pod Anti-Affinity

Pod Anti-Affinity is a setting that lets you direct where pods get placed within the cluster to avoid them all being crammed onto a single node or zone. This matters a lot for guaranteeing service availability, because if all pods of the same application sit on one node and that node has an incident, the entire service goes down.

By using Anti-Affinity, you can specify that pods need to be spread across multiple different nodes, reducing risk when a node has an incident. Anti-Affinity is commonly applied to applications that need high availability and handle heavy load. This setting boosts fault tolerance and improves the system's overall availability.

Sample Pod Anti-Affinity config for an nginx deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: anti-affinity-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - myapp
            topologyKey: "kubernetes.io/hostname" # Requires pods to not sit on the same node
      containers:
      - name: nginx
        image: nginx

Wrapping up

So, in this post I've covered the Kubernetes features that help increase and guarantee an application's high availability:

  • Readiness & Liveness Probes
  • Resource request & limit
  • Pod Anti-affinity
  • Pod Disruption Budget
  • Horizontal Pod Autoscaling
  • PostStart & PreStop Hook
  • Deployment Strategy

There are of course plenty of other factors that affect an application's availability, like the codebase, physical infrastructure, etc., which we also need to pay attention to. But from a DevOps perspective managing the application layer, getting the configurations above right already contributes about 80% of an application's availability.

Hope this post gave you some new ideas for your own work! If you found it useful, please give it an Upvote in the top-left corner and Follow me to catch more posts. Have a nice day!

Have thoughts on this?

I'd love to hear your perspective. Send me a message.

Get in touch