workloads

Choosing the Perfect Kubernetes Workload: A Practical Guide for Application Success¶

Gulcan Topcu

Jul 12, 2024

20 min read

🏷️Tagged with:

kubernetes

Choosing the optimal Kubernetes workload can be a critical factor in the success of your application deployment. Mismatched workloads often lead to performance bottlenecks, unnecessary complexity, and wasted resources.

This comprehensive guide is designed to streamline your decision-making process. We begin with a mindmap offering a high-level overview of the diverse Kubernetes workload landscape, including Deployments, StatefulSets, DaemonSets, CronJobs, Jobs and more.

This visual aid serves as your roadmap to understanding the unique characteristics and ideal use cases of each workload type.

Let’s delve deeper into these workload types.

  • Deployments: These are your go-to solution for managing stateless applications, ensuring high availability, and seamless scaling.

  • StatefulSets: Ideal for applications requiring persistent storage and stable network identities, StatefulSets are crucial for maintaining data consistency and reliability.

  • DaemonSets: Designed to run a pod on every node in your cluster, these are perfect for essential system-level tasks like logging and monitoring.

  • CronJobs: Automate your scheduled tasks with CronJobs, ensuring timely backups, batch processing, and other recurring operations.

  • Jobs: If you need to execute tasks that run to completion, such as data processing or short-lived scripts, Jobs are your solution.

Understanding these distinct workload types is the first step towards optimizing your Kubernetes deployments and ensuring your applications perform at their best.

Now, let’s explore some specific scenarios where these workloads shine, demonstrating their real-world applications.

diagram illustrating the diverse range of Kubernetes workloads at our disposal

Practical Use Cases: Putting Kubernetes Workloads to Work¶

Scenario: Scaling Your Web Application (Deployment)¶

Imagine you’re running a web application that demands high availability, effortless scalability, and swift recovery from failures.

Kubernetes Deployments are the perfect solution for this scenario.

Why Deployments?

Deployments excel at managing replica sets, rollouts, and rollbacks, ensuring your stateless application remains highly available and resilient to failures.

The YAML manifest below demonstrates how a Deployment can be configured to create three replicas of your web application, providing redundancy and the ability to scale horizontally.

The deployment mechanism ensures smooth, zero-downtime updates and automatically replaces failed pods, guaranteeing uninterrupted service.

# Scaling Your Web Application (Deployment)
# The Deployment ensures high availability and self-healing for stateless applications.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  namespace: kuberada
spec:
  replicas: 3  # Specifies three replicas for redundancy and horizontal scaling.
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: mywebapp
        image: nginx:latest  # Container image for the web application.

#an imperative alternative would be
kubectl create deployment webapp --image=nginx -n kuberada
kubectl get po <pod-name> -n kuberada -o jsonpath='{.spec.containers[*].name}'


# Update the Deployment (zero-downtime)
kubectl set image deployment/webapp mywebapp=nginx:1.19.2
kubectl rollout status deployment/webapp 

#get the revisions
kubectl rollout history deployment webapp


# Test Self-Healing
kubectl delete pod <pod-name>
kubectl get pods  # Verify new pod is created

#manual scaling
kubectl scale deployment webapp --replicas=2 -n kuberada

Let’s roll out the nginx 1.19.2 version for our web app deployment.

gulcan@topcu:~$ k rollout history deployment webapp 
deployment.apps/webapp 
REVISION  CHANGE-CAUSE
1         <none>  # revision of initial creation
2         <none>  # image name change revisiom

You can also scale the replicas to 2 manually. All you’re doing is to edit the spec.replicas section to the value you desire.

Yes you can edit the live object too.

Now our deployment controls 2 replicas and exit status from kubectl rollout is 0.

This quick demo takes us further and explores some advanced Deployment techniques that can elevate your Kubernetes knowledge.

Taking Deployments to the Next Level: Mastering Advanced Techniques¶

Kubernetes Deployments offer robust features beyond essential replica management. Let’s delve into these advanced techniques to optimize your deployments for resilience, scalability, and resource efficiency:

Pod Disruption Budgets (PDBs): Ensuring High Availability During Disruptions¶

Imagine a scenario where an unexpected node failure or a cluster upgrade could disrupt your web application. Pod Disruption Budgets (PDBs) come to the rescue by ensuring a minimum number of pods remain available during such events, safeguarding your application from outages.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: webapp-pdb
spec:
  minAvailable: 2  # Ensures at least 2 pods are always available.
  selector:
    matchLabels:
      app: web

In this example, minAvailable: 2 guarantees that at least two replicas of your web application are always running, even during maintenance. minAvailabe can be either an absolute number or a percentage.

Since we want to protect my webapp deployment with a PDB, we must ensure that the .spec.selector in the deployment manifest is the same.

Horizontal Pod Autoscaling (HPA): Dynamic Scaling for Varying Loads¶

As your web application’s traffic fluctuates, manually scaling pods can become cumbersome. Horizontal Pod Autoscaling (HPA) automates this process by dynamically adjusting the number of pods based on metrics like CPU usage. This ensures your application has the resources it needs without over-provisioning.

Let’s add CPU and memory resource requests.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  namespace: kuberada
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: mywebapp
        image: nginx:latest
        requests:
          memory: "64Mi"
          cpu: "100m"
        limits:
          memory: "128Mi"
          cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: webapp-svc
  labels:
    app: web
spec:
  ports:
  - port: 80
  selector:
    app: web
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: webapp
  namespace: kuberada
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: webapp
  minReplicas: 3
  maxReplicas: 7
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50  # Target 50% CPU utilization.

#running metrics server using helm
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm repo update
helm upgrade --install --set args={--kubelet-insecure-tls} metrics-server metrics-server/metrics-server --namespace kube-system

#deploying hpa
kubectl apply -f web-app-hpa.yaml
kubectl get hpa

#an imperative alternative would be
kubectl autoscale deploy webapp --cpu-percent=50 --min=3 --max=7

#check the current status of the hpa
kubectl get hpa

kubectl run -i --tty load --rm --image=busybox:1.36 --restart=Never -- /bin/sh -c "watch -n 0.000001 wget -q -O- http://webapp-svc"

kubectl get hpa webapp-hpa --watch

This HPA scales between 3 and 7 replicas of our webapp deployment based on CPU utilization to match demand (aiming for 50%). You can explore autoscaling on multiple metrics.

Once we created the hpa, the replicas scaled up to 3 (athough we specified 2 in the deployment file).

It’s time to see how our application behaves under expected load conditions.

webapp   Deployment/webapp   cpu: 36%/50%   3         7         3          10m
webapp   Deployment/webapp   cpu: 35%/50%   3         7         3          10m
webapp   Deployment/webapp   cpu: 44%/50%   3         7         3          11m
webapp   Deployment/webapp   cpu: 51%/50%   3         7         3          11m
webapp   Deployment/webapp   cpu: 53%/50%   3         7         3          11m
webapp   Deployment/webapp   cpu: 57%/50%   3         7         3          11m
webapp   Deployment/webapp   cpu: 66%/50%   3         7         4          12m
webapp   Deployment/webapp   cpu: 57%/50%   3         7         4          12m
webapp   Deployment/webapp   cpu: 55%/50%   3         7         5          12m
webapp   Deployment/webapp   cpu: 50%/50%   3         7         5          12m
webapp   Deployment/webapp   cpu: 44%/50%   3         7         5          13m
webapp   Deployment/webapp   cpu: 45%/50%   3         7         5          13m

We generated load with a busybox client. Autoscaling the replicas took a couple of minutes. We saw the CPU increase to 66%, and the deployment was gradually resized to 4 and 5 replicas.

Then we stopped the load testing, since there was no active clients CPU turned back to 0 after a couple minutes , replicas also scaled down to 3 (as we specified in the hpa’s spec.minReplicas section)

gulcan@topcu:~$ k get hpa webapp --watch
NAME     REFERENCE           TARGETS       MINPODS   MAXPODS   REPLICAS   AGE
webapp   Deployment/webapp   cpu: 1%/50%   3         7         5          20m
webapp   Deployment/webapp   cpu: 0%/50%   3         7         5          20m
webapp   Deployment/webapp   cpu: 0%/50%   3         7         5          23m
webapp   Deployment/webapp   cpu: 0%/50%   3         7         4          24m
webapp   Deployment/webapp   cpu: 0%/50%   3         7         4          24m
webapp   Deployment/webapp   cpu: 0%/50%   3         7         3          24m
webapp   Deployment/webapp   cpu: 0%/50%   3         7         3          25m

Resource Limits and Requests: Fine-Tuning Resource Allocation¶

Kubernetes allows you to specify your pods’ resource limits (maximums) and requests (guarantees). This prevents resource contention and ensures your applications have the necessary resources without over-consuming them.

...
  requests:
    memory: "64Mi"
    cpu: "100m"
  limits:
    memory: "128Mi"
    cpu: "200m"

Our deployment requests 64MiB memory and 100 milliCPU, with limits of 128MiB and 200 milliCPU per pod.

Init Containers: Preparing the Environment for Your Application¶

Init containers are specialized containers that run before your main application container. They’re perfect for tasks like fetching configuration files, initializing databases, or setting up the environment.

Example Deployment with Init Container (app-with-init.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-with-init
# ... (rest of the manifest is the same as before)
    spec:
      volumes:
      - name: shared-logs
        emptyDir: {}
      initContainers:
      - name: init-myservice
        image: busybox:1.36
        command:
          - 'sh'
          - '-c'
          - 'mkdir -p /var/log/nginx && echo "Init container started" > /var/log/nginx/init.log && chmod -R 777 /var/log/nginx && for i in $(seq 1 20); do echo "Init container is running $i" >> /var/log/nginx/init.log; sleep 1; done'
        volumeMounts:
        - name: shared-logs
          mountPath: /var/log
      containers: 
        # ... your main container definition
        volumeMounts:
        - name: shared-logs
          mountPath: /var/log

#display the logs of the init container
kubectl logs <pod-name> -c init-myservice -n kuberada

Let’s add an init container to our webapp deployment.

shared-logs is an emptyDir volume that provides shared storage that is accessible to all containers in the pod. It is ephemeral and will be lost when the pod is removed.

The init container creates the directory /var/log/nginx if it doesn’t exist, sets permissions, appends 20 messages to /var/log/nginx/init.log, and pauses for 1 second between each iteration.

We didn’t specify and resource request and limit for the init container because they share the same resources as the main container.

Sidecar Containers: Enhancing Your Application with Auxiliary Tasks¶

Sidecar containers run alongside your main application container, providing complementary functionality like logging, monitoring, or service mesh integration.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-with-sidecar
# ... (rest of the manifest is the same as before)
    spec:
      containers:
      - name: main-container
        # ... your main container definition
      - name: log-reader
        image: busybox:1.36
        command: ['sh', '-c', 'tail -f /var/log/init.log']
        volumeMounts:
        - name: shared-logs
          mountPath: /var/log
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"

#get all the container running in a deployment
kubectl get deploy webapp -n kuberada -o jsonpath='{.spec.template.spec.containers[*].name}'

#check the shared volume
kubectl exec -it <pod-name> -c mywebapp -n kuberada -- sh

#check the logs of the init container
kubectl logs <pod-name> -c init-myservice -n kuberada

Now we have one init and two containers (main app is nginx and a busybox as a sidecar) in our deployment. Let’s read the messages init container written to init.log.

Upon accessing the contents of init.log from the log-reader container, we verified that our sidecar container continuously displayed new entries from the init.log file.

Now, let’s shift our focus to DaemonSets, another powerful workload type designed for specific use cases.

Monitoring Every Node (DaemonSet)¶

A logging or monitoring agent must run on every node in your cluster.

DaemonSets guarantee that one pod runs on each node, including newly added nodes, making them ideal for system-level tasks.

This DaemonSet ensures a monitoring agent is deployed on every node, providing node-specific monitoring and log collection essential for system health.

# Monitoring Every Node (DaemonSet)
# DaemonSet ensures a pod is run on each node for system-level tasks like monitoring.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: monitoring-agent
spec:
  selector:
    matchLabels:
      app: monitor
  template:
    metadata:
      labels:
        app: monitor
    spec:
      containers:
      - name: monitor-container
        image: prom/prometheus:latest  # Container image for the monitoring agent.

# Apply the DaemonSet
kubectl apply -f monitoring-agent-daemonset.yaml

# Verify DaemonSet and the pods running on each node
kubectl get daemonsets
kubectl get pods -o wide

We created a 3 node cluster, installed prometheus monitoring agent and verified automatic scheduling of the ds pod.

Scheduled Tasks (CronJob)¶

You need to run a nightly database backup.

CronJobs allow you to schedule jobs to run at specified times, automating repetitive tasks.

This CronJob schedules a backup to run every night at midnight, ensuring regular and automated database backups without manual intervention.

# Scheduled Tasks (CronJob)
# CronJob schedules jobs to run at specified times, automating repetitive tasks.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: log-deletion
  namespace: kuberada
spec:
  schedule: "0 0 * * *"  # Schedule to run every day at midnight.
  jobTemplate:
    spec:
      template:
        spec:
          failedJobsHistoryLimit: 0
          containers:
          - name: log-cleaner
            image: busybox:1.36
            command:
              - 'sh'
              - '-c'
              - 'find /var/log/nginx -type f -name "*.log" -mtime +7 -delete'
            volumeMounts:
            - name: shared-logs
              mountPath: /var/log/nginx
          restartPolicy: OnFailure
          volumes:
          - name: shared-logs
            emptyDir: {}

# Apply the CronJob
kubectl apply -f nightly-backup-cronjob.yaml

#an imperative alternative would be
kubectl create cronjob log-deletion --schedule="0 0 * * *" --image=busybox:1.36 -n kuberada -- /bin/sh -c 'find /var/log/nginx -type f -name "*.log" -mtime +7 -delete'


# Verify CronJob and check its history after midnight
kubectl get cronjobs
kubectl get jobs

#Manually trigger the CronJob to run the log deletion job
kubectl create job --from=cronjob/log-deletion manual-log-deletion -n kuberada


# Monitor the job execution and review logs
kubectl logs <job-pod-name>

The schedule for a CronJob in Unix-like systems (including Kubernetes) is specified using five fields that define the time and frequency of execution. Each field can contain specific values, ranges, or special characters to define the schedule.

For instance, we can use * * * * * for every minute, */5 * * * * for every 5 minutes or 0 12 * * * for everyday at noon.

Our cj is scheduled to delete the logs everyday 12:00 AM (midnight) in the 24-hour clock system with keeping the failed job history limit to none, default is one.

* * * * *
│ │ │ │ │
│ │ │ │ └─── Day of the week (0 - 7) (Sunday = 0 or 7)
│ │ │ └───── Month (1 - 12)
│ │ └─────── Day of the month (1 - 31)
│ └───────── Hour (0 - 23)
└─────────── Minute (0 - 59)

Stateful Applications (StatefulSet)¶

You’re deploying a distributed database that requires persistent storage and ordered pod deployment.

StatefulSets manage stateful applications by providing stable network identities, persistent storage, and ordered scaling.

This StatefulSet ensures each database pod has a unique identifier, stable storage, and maintains order during scaling and updates, crucial for data integrity and consistency.

# Stateful Applications (StatefulSet)
# StatefulSet manages stateful applications, providing stable identities and persistent storage.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: kuberada
spec:
  serviceName: "mysql"
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: kuberada
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
        - name: init-script
          mountPath: /docker-entrypoint-initdb.d
        livenessProbe:
          exec:
            command:
            - mysqladmin
            - ping
            - -h
            - localhost
          initialDelaySeconds: 30
          timeoutSeconds: 10
      volumes:
      - name: init-script
        configMap:
          name: mysql-init
  volumeClaimTemplates:
  - metadata:
      name: mysql-persistent-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

# Scale the StatefulSet and observe the order of pod creation
kubectl scale statefulset database --replicas=4

kubectl get pods -w

#verify Database and User
SHOW DATABASES;
USE workloads;
SHOW TABLES;
SELECT User, Host FROM mysql.user WHERE User = 'kuberada';

#connect as kuberada user
kubectl exec -it  <po-name> -n kuberada -- bash
mysql -u kuberada -pkuberada

The init script only creates the database and user,so there will be no tables within the database.

You can verify the database and user creation by connecting to the MySQL instance.

In this StatefulSet, we automated a 3-replica MySQL cluster with stable network identities, ordered deployment, persistent storage, and an initialization script for setting up the database workloads and user kuberada.

One-Time Tasks (Job)¶

Let’s say you must send your team a critical one-time email notification about an upcoming system maintenance. Kubernetes Jobs are tailor-made for this scenario.

A Job will spin up a single pod, execute the task of sending the email, and then gracefully terminate upon completion. There’s no need to worry about restarts or ongoing management; the Job ensures your notification goes out once and only once.

# One-Time Tasks (Job)
# Jobs run tasks to completion without restarting on failure.

apiVersion: batch/v1
kind: Job
metadata:
  name: email-job
spec:
  template:
    spec:
      containers:
      - name: email-container
        image: email-sender:latest  # Container image for the email sender.
        command: ["send-email"]  # Command to run in the container.
      restartPolicy: Never  # Job completes without restarting on failure.

This Job example demonstrates a simple yet powerful way to automate tasks within your Kubernetes environment. Whether it’s sending notifications, performing data cleanup, or executing any other finite task, Jobs provide a reliable and efficient mechanism to ensure your operations run smoothly.

Let’s shift our focus from one-time tasks to the broader landscape of Kubernetes workloads and their diverse applications.

The diagram below serves as a visual reference for the different Kubernetes workload types and their typical use cases, aiding your understanding as we explore practical examples.

Decision Time: Your Kubernetes Workload Flowchart¶

This flowchart can guide you through the decision-making process, asking key questions about your application’s requirements to determine the most appropriate Kubernetes workload type.

Workloads in Complex Production Environments¶

Let’s consider how Kubernetes workloads are used in complex production environments.

Imagine an e-commerce platform during a high-traffic sales event. To manage the surge in user activity, the platform uses Deployments to scale web applications efficiently. StatefulSets ensure that databases remain reliable and data stays consistent despite the heavy load. Meanwhile, Jobs handle background tasks like order processing and data cleanup, ensuring these operations run smoothly without impacting the user experience.

In a microservices architecture, Kubernetes workloads are equally essential. Deployments manage stateless services such as user authentication and product catalogs, allowing these services to scale and update independently. StatefulSets manage stateful services like databases and message brokers, which need stable network identities and persistent storage. DaemonSets deploy logging and monitoring agents on every node, providing comprehensive system monitoring and log collection to maintain the architecture’s health and performance.

These examples show how Kubernetes workloads optimize different parts of a production environment, ensuring performance, reliability, and scalability.

Key Takeaways¶

Let’s recap the essential lessons from this guide:

  • Each workload type serves a specific purpose. Assess your application requirements carefully.

  • Selecting the right workload type can optimize performance, resource utilization, and application stability.

  • Implement Thoughtfully: Ensure your manifests are correctly configured to take full advantage of Kubernetes’ capabilities.

  • Monitor and Adapt: Continuously monitor your workloads and adapt as necessary to maintain optimal performance and resource usage.

Choosing the right Kubernetes workload can be daunting, but with this guide, you have the tools to make informed decisions that will set your applications up for success.

Remember, the key to success is not just deploying workloads, but deploying them intelligently.

Enjoyed this read?

If you found this guide helpful, follow me on:

  • LinkedIn to get the latest updates.

  • Medium for even more Kubernetes insights and discussions.

  • Kuberada, for in-depth articles.

Until next time, happy deploying!