DEV Community

Cover image for Building a Custom Kubernetes Scheduler(with Simple ShellScripts): A Hands-On Lab Journey
Hardik Arora
Hardik Arora

Posted on

Building a Custom Kubernetes Scheduler(with Simple ShellScripts): A Hands-On Lab Journey

As Kubernetes continues to dominate container orchestration, understanding how its scheduler works becomes crucial for any DevOps engineer.This hands-on lab demonstrates the inner workings of Kubernetes scheduling and walks through creating a custom scheduler from scratch.

Understanding the Kubernetes Scheduler

Before diving into the terminal, it's important to understand the component at play. The Kube-Scheduler serves as the brain behind pod placement in Kubernetes. It follows a three-phase process:

Filtering - Eliminates nodes that don't meet requirements

Scoring - Ranks suitable nodes based on various factors

Binding - Creates the connection between pod and chosen node

The scheduler considers CPU, memory, node affinity, taints, tolerations, and various other constraints to make intelligent placement decisions.

Lab Setup: Creating the First Scheduled Pod

The lab begins by templating an nginx pod that will later be targeted with a custom scheduler:

kubectl run nginx --image=nginx -o yaml --dry-run=client | tee nginx_scheduler.yaml

This command generates a pod manifest without actually creating the pod. The --dry-run=client flag proves perfect for templating purposes.

Exploring the schedulerName Option

Kubernetes provides a powerful but lesser-known feature: the ability to specify which scheduler should handle a pod. The lab explores this capability:

kubectl explain pod.spec | more

Among the various pod specification options, schedulerName appears - a string field that allows targeting a specific scheduler. This becomes the gateway to custom scheduling logic.

Configuring a Custom Scheduler

The next step involves updating the nginx pod manifest to use a custom scheduler called my-scheduler:

apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
schedulerName: my-scheduler
containers:

  • image: nginx name: nginx resources: {} dnsPolicy: ClusterFirst restartPolicy: Always status: {} EOF

The key addition here is schedulerName: my-scheduler under the spec section.

Figure 2: The nginx_scheduler.yaml file with schedulerName specified

Applying the Configuration

The lab proceeds with applying this configuration:

kubectl apply -f nginx_scheduler.yaml

Checking the pod status reveals an interesting result:

kubectl get pods -o wide

Observation: The pod remains stuck in Pending state. This occurs because there's no scheduler called my-scheduler running in the cluster. The pod waits patiently for its designated scheduler to pick it up.

Figure 1: The nginx pod remains in Pending state as it waits for the my-scheduler

Building the Custom Scheduler

While production schedulers are typically written in Golang and deeply integrated with the Kubernetes API, this lab uses a bash script for educational purposes. This approach helps demonstrate the scheduler's workflow without getting lost in complex code.

First, the necessary tools are installed:

apt update && apt install -y git jq

Next, the example scheduler repository is cloned:

git clone https://github.com/spurin/simple-kubernetes-scheduler-example.git

The lab then moves into the directory and examines the scheduler script:

cd simple-kubernetes-scheduler-example
more my-scheduler.sh

Understanding the Scheduler Logic

The script performs several key operations:

Queries available nodes using jsonpath to extract node names

Identifies pods waiting for my-scheduler

Selects a node (randomly in this simple example)

Makes a binding request to the API server

To understand how the JSON traversal works, the raw node data can be examined:

kubectl get nodes -o json

This reveals the structure the script navigates through - items as a list, each with metadata and a name field.

Running the Custom Scheduler

The exciting moment arrives when the custom scheduler is executed:

./my-scheduler.sh

The script picks up the pending pod and binds it to a node. After pressing Ctrl+C, the pod status is checked again:

kubectl get pods -o wide

Success! The pod is now running on a specific node, scheduled by the custom scheduler.

Figure 3: The my-scheduler.sh script in action, binding the pod to a node

Figure 4: The nginx pod now shows as Running with a node assignment

The Power of nodeName: Bypassing the Scheduler

Kubernetes also provides a way to bypass scheduling altogether using the nodeName field. This directly assigns a pod to a specific node without any scheduler involvement.

The lab first returns to the previous directory:

cd ..

The options for nodeName under pod.spec can be reviewed:

kubectl explain pod.spec | more

Direct Node Assignment with nodeName

The yaml file is then updated to directly specify worker-2 as the target node:

cat < nginx_scheduler.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
nodeName: worker-2
containers:

  • image: nginx name: nginx resources: {} dnsPolicy: ClusterFirst restartPolicy: Always status: {} EOF

The configuration is applied:

kubectl apply -f nginx_scheduler.yaml

Checking the pod status confirms it has been scheduled directly to worker-2:

kubectl get pods -o wide

Result: The pod appears on worker-2 immediately, bypassing the scheduler entirely. This demonstrates how nodeName provides a direct path from pod specification to node assignment.

Figure 5: The pod scheduled directly to worker-2 using nodeName specification

The pod is removed for the next demonstration:

kubectl delete pod/nginx --now

nodeSelector: Label-Based Scheduling (Further Study)

Another approach to pod placement involves using nodeSelector, which leverages node labels for targeting specific nodes. This method provides more flexibility than nodeName while still maintaining control over placement.

Examining Node Labels

The worker-1 node is examined to view its labels:

kubectl describe node/worker-1 | more

The labels section at the top reveals various metadata, including kubernetes.io/hostname=worker-1. This label becomes the selector for targeting this specific node.

Figure 6: Examining the labels on worker-1 node

Figure 7: The kubernetes.io/hostname label that will be used in nodeSelector

Configuring nodeSelector

The yaml file is updated to use nodeSelector with the hostname label:

cat < nginx_scheduler.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
nodeSelector:
kubernetes.io/hostname: worker-1
containers:

  • image: nginx name: nginx resources: {} dnsPolicy: ClusterFirst restartPolicy: Always status: {} EOF

The update is applied:

kubectl apply -f nginx_scheduler.yaml

Verification shows the pod has been scheduled to worker-1:

kubectl get pods -o wide

Observation: Unlike nodeName, nodeSelector still goes through the scheduler but constrains the scheduling decision to nodes matching the specified labels. This approach offers more flexibility and is considered a best practice for most use cases.

Figure 8: The nodeSelector configuration using the hostname label

Cleanup

Finally, all lab resources are cleaned up:

kubectl delete pod/nginx --now
rm -rf simple-kubernetes-scheduler-example
rm -rf nginx_scheduler.yaml

Key Takeaways

Through this lab, several important concepts are demonstrated:

Scheduler workflow: Filtering → Scoring → Binding

Custom schedulers: How to create and target custom scheduling logic

The binding process: How pods get assigned to nodes

Direct node assignment: Using nodeName to bypass scheduling entirely

Label-based scheduling: Using nodeSelector for flexible, constraint-based placement

Scheduling Methods Compared

The lab explored three distinct approaches to pod scheduling:

Custom Scheduler (schedulerName): Provides complete control over scheduling logic, ideal for specialized placement algorithms

Direct Assignment (nodeName): Bypasses the scheduler entirely, useful for debugging or specific fixed placements

Label-Based Selection (nodeSelector): Combines scheduler intelligence with constraint-based node selection, offering the best balance for most production scenarios

When Would One Use This?

Custom schedulers prove valuable when:

Specialized placement logic is needed for specific workloads

Default scheduler constraints don't meet requirements

Advanced placement strategies are being implemented (GPU scheduling, network topology awareness, etc.)

Conclusion

Understanding Kubernetes scheduling unlocks a deeper level of cluster control. While the default scheduler handles most scenarios effectively, knowing how to implement custom scheduling provides the flexibility to optimize for specific use cases.

The ability to bypass or customize scheduling demonstrates Kubernetes' modular design philosophy - providing sensible defaults while allowing deep customization when needed.

This lab successfully demonstrates that the scheduler's "magic" is actually a well-designed, understandable process that can be customized to meet unique requirements.

What's Next?

Having mastered Kubernetes scheduling mechanisms, the next topic in this series will explore Kubernetes Storage - another critical component for running stateful workloads in production environments. 🚀

For those interested in diving deeper into Kubernetes internals, this hands-on approach provides an excellent foundation for understanding how the platform orchestrates containerized workloads at scale.

Top comments (0)