At first glance, setting a podAffinity
with the NotIn
operator might seem equivalent to setting a podAntiAffinity
with the In
operator. But in Kubernetes, these two configurations can lead to dramatically different scheduling behaviors.
🤓 🤓 🤓 Nerd alert
This blog post is about a super nerdy edge-case that I’ve discussed sometime ago
with Wojtek from ML-Workout.pl.
The Question
Does setting the following podAffinity
on a Deployment
:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: NotIn
values:
- myapp
topologyKey: "kubernetes.io/hostname"
have the same effect as setting this podAntiAffinity
?
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- myapp
topologyKey: "kubernetes.io/hostname"
The Answer
No, they are not the same ⬛️.
Here’s why:
-
Understanding
podAffinity
withNotIn
:- What it means: “Schedule this pod on a node where there is at least one pod whose
app
label is notmyapp
.” - Implication: The node must have at least one pod with an
app
label that is different frommyapp
.
- What it means: “Schedule this pod on a node where there is at least one pod whose
-
Understanding
podAntiAffinity
withIn
:- What it means: “Avoid scheduling this pod on nodes where there is any pod whose
app
label ismyapp
.” - Implication: The pod can be scheduled on any node that doesn’t have pods labeled
app: myapp
.
- What it means: “Avoid scheduling this pod on nodes where there is any pod whose
Deep walk-through
Let’s walk through a practical example to see how these configurations behave differently.
-
Starting Point
You have 2 or more nodes with no pods scheduled on them—just plain nodes with no taints or special configurations. -
Deploy
app:affinity
You apply a Deployment labeledapp: affinity
with the followingpodAffinity
rule:podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: NotIn values: - myapp topologyKey: "kubernetes.io/hostname"
-
Result:
The pod does not start and stays inPending
.
Why? Because there are no pods on any nodes, so there’s no node where a pod with anapp
label not equal tomyapp
is running. The affinity rule requires at least one such pod. -
Deploy
app:anti
:
You deploy another Deployment labeledapp: anti
with the followingpodAntiAffinity
rule:podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - myapp topologyKey: "kubernetes.io/hostname"
-
Result:
The pod schedules immediately on one of the nodes.
Why? Because there are no pods withapp: myapp
running anywhere, so there’s nothing to avoid. -
Recheck
app:affinity
:
Afterapp:anti
is scheduled, theapp:affinity
pod now schedules on the same node asapp:anti
.
Why? Because the node now has a pod (app: anti
) with anapp
label not equal tomyapp
, satisfying the affinity rule. -
Increase the replicas of
app:affinity
to 2. -
Result:
The new pod also schedules on the same node, even if it’s getting crowded 🙃
Why? It’s the only node that satisfies thepodAffinity
condition.
DON’T BELIEVE ME, CHECK IT OUT BY YOURSELF! 👀
Hands-On Example: Experimenting with Pod Affinity and Anti-Affinity
Ready to see this in action? Let’s walk through a quick hands-on example using k3d, a lightweight Kubernetes distribution that runs inside Docker. This will help you visualize how these affinity rules affect pod scheduling.
Step 1: Install k3d
First, install k3d by running the following command:
curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash
# See source: https://k3d.io/v5.7.4/#installation
Verify the installation:
k3d version
Step 2: Create a k3d Cluster
Create a new k3d cluster with a single node:
k3d cluster create mycluster --servers 1
This command creates a Kubernetes cluster named mycluster
with one server node.
Step 3: Set Up kubectl
Make sure you have kubectl
installed and configured to interact with your new cluster:
kubectl cluster-info
Step 4: Experiment with Pod Affinity and Anti-Affinity
4.1 Apply the affinity.yaml
Save the following content as affinity.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: affinity-app
labels:
app: affinity-app
spec:
replicas: 1
selector:
matchLabels:
app: affinity-app
template:
metadata:
labels:
app: affinity-app
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: NotIn
values:
- affinity-app
topologyKey: "kubernetes.io/hostname"
containers:
- name: affinity-app
image: alpine:3.14
command: ["/bin/sh"]
args: ["-c", "while true; do echo 'Running' $(date); sleep 5; done"]
terminationGracePeriodSeconds: 1
Apply it:
kubectl apply -f affinity.yaml
4.2 Observe the Pending State
Check the status of the pods:
kubectl get pods
You should see that the affinity-app
pod is in a Pending
state.
4.3 Clean Up
Delete the deployment:
kubectl delete -f affinity.yaml
4.4 Apply the anti.yaml
Save the following content as anti.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: anti-app
labels:
app: anti-app
spec:
replicas: 1
selector:
matchLabels:
app: anti-app
template:
metadata:
labels:
app: anti-app
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- anti-app
topologyKey: "kubernetes.io/hostname"
containers:
- name: anti-app
image: alpine:3.14
command: ["/bin/sh"]
args: ["-c", "while true; do echo 'Running' $(date); sleep 5; done"]
terminationGracePeriodSeconds: 1
Apply it:
kubectl apply -f anti.yaml
4.5 Observe Pod Scheduling
Check the pods again:
kubectl get pods
You should see that the anti-app
pod is running.
4.6 Clean Up and Wait
Delete the deployment:
kubectl delete -f anti.yaml
Wait until all pods are terminated:
kubectl get pods --watch
Press Ctrl+C
to stop watching once all pods are gone.
4.7 Reapply affinity.yaml
Apply the affinity.yaml
again:
kubectl apply -f affinity.yaml
Check the pods:
kubectl get pods
Again, the affinity-app
pod should be in a Pending
state.
4.8 Apply anti.yaml
Again
Now, apply anti.yaml
without deleting affinity.yaml
:
kubectl apply -f anti.yaml
Observe the Magic 🧞♂️🪄
Check the pods:
kubectl get pods
This time, you’ll see that both affinity-app
and anti-app
pods are running. The affinity-app
pod could finally schedule because the node now has a pod (anti-app
) with an app
label not equal to affinity-app
, satisfying the affinity condition.
Optional: Scale Up and Add Nodes
If you want to explore further:
-
Add More Nodes:
k3d node create extra-node --cluster mycluster
-
Increase Replicas:
Edit
affinity.yaml
andanti.yaml
to increase thereplicas
count:spec: replicas: 2 # Increase this number
Reapply the deployments:
kubectl apply -f affinity.yaml kubectl apply -f anti.yaml
Observe how the pods are scheduled across the nodes based on the affinity and anti-affinity rules.
Clean Up
When you’re done experimenting, delete the cluster:
k3d cluster delete mycluster
By following these steps, you can see firsthand how Kubernetes handles podAffinity
with NotIn
and podAntiAffinity
with In
operators. This practical exercise should cement your understanding and help you avoid scheduling surprises in your own clusters.
Enjoyed this hands-on tutorial & speaking Polish 🇵🇱? Check out more on my YouTube channel ML-Workout for Machine Learning and MLOps tutorials!
Key Takeaways
-
Affinity Requires Presence:
podAffinity
rules require the presence of pods matching the criteria. -
Anti-Affinity Requires Absence:
podAntiAffinity
rules require the absence of pods matching the criteria. -
The
NotIn
Operator in Affinity: When used withpodAffinity
, theNotIn
operator looks for nodes where there are pods whose labels do not match the specified values, but there must be at least one pod present.
Don’t Let This Happen to You
Understanding these subtle differences can save you hours of debugging. Always double-check your affinity rules and remember how Kubernetes interprets them.
Please share & comment! 🗣️🗣️🗣️
Comments