Intro
Pod affinity and anti-affinity are Kubernetes scheduling features that let you control which nodes your pods land on, based on the labels of other pods already running on those nodes. They are powerful for performance, high availability, and compliance, but they also introduce security risks if misconfigured. For example, an overly broad anti-affinity rule can prevent critical pods from ever being scheduled, causing a denial of service. An overly permissive affinity rule might unintentionally co-locate sensitive workloads with less trusted pods, increasing the blast radius of a compromise.
This guide is for platform engineers, DevOps consultants, and technical startup teams who need to harden Kubernetes pod affinity and anti-affinity configurations without breaking their clusters. We focus on practical, verifiable steps: observe before changing, limit the blast radius, use placeholders instead of secrets, verify results, and document recovery paths.
By the end of this article, you will be able to:
- Inventory your current Kubernetes version and environment to know what affinity features are available.
- Apply safe configuration changes using manifests with explicit, minimal rules.
- Verify that your affinity and anti-affinity settings are working as intended using
kubectlcommands. - Diagnose and recover from common failure modes, such as unschedulable pods.
- Follow an operations checklist to maintain security over time.
Throughout, we use concrete commands, manifest snippets, and expected outputs so you can follow along on your own cluster.
Version and Environment Inventory
Before hardening pod affinity and anti-affinity, you must know your Kubernetes version and environment specifics. Affinity and anti-affinity have been stable since Kubernetes 1.14, but earlier versions may lack some fields (e.g., namespaceSelector for cross-namespace affinity). Also check if you use a managed Kubernetes service (EKS, GKE, AKS) or a self-managed cluster, as this affects how you apply changes and access control.
Key prerequisites for this guide:
- Kubernetes cluster version 1.19 or later (to use all affinity fields safely).
kubectlinstalled and configured with appropriate permissions (at leastget,describe,logs, andapplyon pods, deployments, and namespaces).- A test namespace (e.g.,
affinity-test) where you can safely experiment without affecting production.
Step 1: Check cluster version
Run:
kubectl version --short
Expected output similar to:
Client Version: v1.27.3
Server Version: v1.27.3
If server version is below 1.14, you need to upgrade before using advanced affinity features.
Step 2: Observe current scheduling state
Before making any changes, capture the current state. For example, list all pods with their nodes and status:
kubectl get pods -A -o wide
This shows NODE, STATUS, and AGE. Note any pods that are already using affinity rules:
kubectl get pods -A -o json | jq '.items[] | select(.spec.affinity != null) | .metadata.name'
This read-only command reveals existing affinity usage without modifying anything.
Step 3: Protect credentials and private material
When working with manifests, avoid embedding secrets. Use placeholders and environment variables. We will discuss secret management later.
Step 4: Smallest justified change
For your first hardening task, choose a single deployment and a single affinity rule. Do not rewrite all scheduling policies at once.
Practical check:
kubectl get pods -n <namespace> -o wide
kubectl describe pod <pod-name> -n <namespace>
In the describe output, look for the Events section. If a pod is unschedulable due to affinity, you will see a message like:
0/3 nodes are available: 3 node(s) didn't match pod affinity/anti-affinity rules.
This is a clear signal that your rule is too restrictive.
Example environment inventory:
Let's create a test namespace and a simple deployment to use throughout this guide.
apiVersion: v1
kind: Namespace
metadata:
name: affinity-test
Apply it:
kubectl apply -f namespace.yaml
Create a simple nginx deployment with three replicas:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-baseline
namespace: affinity-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-baseline
template:
metadata:
labels:
app: nginx-baseline
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
Apply and verify:
kubectl apply -f nginx-baseline.yaml
kubectl rollout status deployment/nginx-baseline -n affinity-test
Expect:
deployment "nginx-baseline" successfully rolled out
Now you have a baseline for comparison.
Safe Configuration Path
Now we harden the deployment by adding affinity and anti-affinity rules, but in a controlled way. The safe path involves:
- Define clear objectives (e.g., spread replicas across nodes, keep sensitive pods away from general pods).
- Use labels to target pods appropriately.
- Apply one manifest at a time and verify each change.
- Keep the blast radius small by starting with a single namespace and a limited number of replicas.
Step 1: Understand affinity types
- Node affinity is about which nodes a pod can be scheduled on based on node labels. (Not the focus here, but often used together.)
- Pod affinity attracts pods to each other based on pod labels.
- Pod anti-affinity repels pods from each other.
For security hardening, anti-affinity is often more critical because it prevents co-location of sensitive workloads. For example, you might not want your payment processing pods on the same node as your public-facing web pods.
Step 2: Add anti-affinity to the baseline deployment
We will modify the nginx deployment to spread its pods across nodes using podAntiAffinity with preferredDuringSchedulingIgnoredDuringExecution. This is a soft rule; it will try to place pods on different nodes, but will still schedule if impossible.
Create a new manifest nginx-anti-affinity.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-spread
namespace: affinity-test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-spread
template:
metadata:
labels:
app: nginx-spread
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx-spread
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
Explanation:
preferredDuringSchedulingIgnoredDuringExecutionis soft; the weight (100) means it is strongly preferred.- The
podAffinityTermselects pods with labelapp=nginx-spread. topologyKey: kubernetes.io/hostnamemeans the rule is per node (hostname).
Apply:
kubectl apply -f nginx-anti-affinity.yaml
kubectl rollout status deployment/nginx-spread -n affinity-test
Step 3: Verify placement
Check which nodes the pods are on:
kubectl get pods -n affinity-test -l app=nginx-spread -o wide
Look at the NODE column. On a multi-node cluster, you should see each pod on a different node. If your cluster has only one node, they will all be on the same node, but the soft rule allows that.
Step 4: Harden with required anti-affinity
For stricter security, use requiredDuringSchedulingIgnoredDuringExecution. This is a hard rule; if it cannot be satisfied, the pod remains unscheduled. This is useful for critical security boundaries, but can cause availability issues if not enough nodes.
Example: create a deployment with required anti-affinity for two replicas:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-required-spread
namespace: affinity-test
spec:
replicas: 2
selector:
matchLabels:
app: nginx-required-spread
template:
metadata:
labels:
app: nginx-required-spread
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx-required-spread
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx:1.25
Apply and then try to scale to 4 replicas on a 2-node cluster:
kubectl apply -f nginx-required-spread.yaml
kubectl scale deployment nginx-required-spread --replicas=4 -n affinity-test
Check pod status:
kubectl get pods -n affinity-test -l app=nginx-required-spread -o wide
You will likely see two pods Running and two Pending. Describe a pending pod:
kubectl describe pod <pending-pod-name> -n affinity-test
In events, you will see:
0/2 nodes are available: 2 node(s) didn't match pod anti-affinity rules.
This demonstrates the trade-off: strict anti-affinity enforces separation but limits density.
Security considerations for the safe path:
- Always use labels that are specific and unlikely to collide with other workloads. Avoid using broad labels like
tier: frontendwithout a unique app label. - Restrict who can modify affinity rules using RBAC. Only users with
patchorupdatepermissions on deployments can change scheduling, so limit that access. - Never put sensitive information in labels; labels are not encrypted and can be read by anyone with pod list access.
- Use namespace isolation: combine affinity with
namespaceSelectorto prevent cross-namespace interference.
Example of namespace-scoped anti-affinity:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- payment-processor
namespaceSelector:
matchLabels:
environment: production
topologyKey: kubernetes.io/hostname
This ensures payment-processor pods do not co-locate with any pod in a namespace labeled environment=production.
Verification and Diagnostics
After applying affinity changes, you need to verify that they work as intended and diagnose any issues. This section covers key commands and interpretation.
1. Check scheduling events
Always describe pods to see scheduler events:
kubectl describe pod <pod-name> -n <namespace>
Look for the Events section. Successful scheduling shows:
Successfully assigned affinity-test/nginx-spread-xyz to node-1
Failure due to affinity shows:
0/3 nodes are available: 3 node(s) didn't match pod affinity/anti-affinity rules.
2. Inspect pod placement against rules
Use kubectl get pods -o wide to verify node distribution. For a more programmatic check, use jq to see which pods are on which nodes:
kubectl get pods -n affinity-test -l app=nginx-spread -o json | jq -r '.items[] | .metadata.name + " -> " + .spec.nodeName'
Expected output:
nginx-spread-abc -> node-1
nginx-spread-def -> node-2
nginx-spread-ghi -> node-3
3. Test pod-to-pod communication (if relevant)
If your affinity is for performance reasons (e.g., keep cache pods near app pods), you may want to test network latency or throughput. Use kubectl exec to run commands inside a pod:
kubectl exec -it <app-pod> -n affinity-test -- curl http://<cache-service>
Verify that the connection is fast or allowed by network policies.
4. Check logs for misconfigurations
If a pod is crash-looping due to affinity-related issues (rare, but possible if an init container depends on pod placement), check logs:
kubectl logs <pod-name> -n <namespace> --previous
5. Validate YAML before applying
Use kubectl apply --dry-run=client or --dry-run=server to validate manifests:
kubectl apply -f nginx-anti-affinity.yaml --dry-run=client
This catches syntax errors without changing the cluster.
Diagnostic scenario: pod stuck in Pending state
Suppose you applied a required anti-affinity rule that cannot be satisfied. The pod stays Pending. Diagnosis steps:
kubectl get pods -n <namespace>to see status.kubectl describe pod <pod-name>to see events.- Check node count:
kubectl get nodes. - Check existing pods' labels:
kubectl get pods -n <namespace> --show-labels. - Determine if your topologyKey is too fine-grained. For example, using
topologyKey: kubernetes.io/hostnameon a single-node cluster will never allow more than one pod with the same label.
Recovery: either increase node count, change to preferred anti-affinity, or relax the label selector.
Failure Modes and Recovery
Even with careful planning, affinity and anti-affinity can cause failures. This section covers common failure modes and how to recover gracefully.
Failure Mode 1: Too-strict anti-affinity causes unschedulable pods
Symptom: Pods remain Pending with event message didn't match pod anti-affinity rules.
Recovery:
- Temporarily scale down the deployment to a number that fits the rule:
kubectl scale deployment <name> --replicas=<lower-number> -n <namespace>
- Or change the anti-affinity from required to preferred by editing the deployment:
kubectl edit deployment <name> -n <namespace>
Replace requiredDuringSchedulingIgnoredDuringExecution with preferredDuringSchedulingIgnoredDuringExecution and add a weight. Save and exit; the deployment rolls out a new revision.
- As a last resort, delete the pending pods;
kubectl delete pod <pending-pod-name> -n <namespace>
The deployment controller recreates them, but if the rule is still unsatisfiable, they will pend again.
Failure Mode 2: Affinity rule accidentally matches unintended pods
Symptom: Pods are scheduled onto nodes with pods they should avoid, or they cluster with unexpected pods.
Cause: Broad label selectors, e.g., using app: web when multiple web apps run in the namespace.
Recovery:
- Inspect labels of running pods:
kubectl get pods -n <namespace> --show-labels
- Update the deployment's affinity label selector to be more specific, e.g.,
app: my-webandtier: frontend. - Apply the updated manifest and watch rollout:
kubectl apply -f updated-deployment.yaml
kubectl rollout status deployment/<name> -n <namespace>
Failure Mode 3: Cross-namespace interference
Symptom: Pods in namespace A affect scheduling of pods in namespace B due to namespaceSelector or broad labels.
Recovery:
- Use
namespaceSelectorto restrict affinity to specific namespaces only if needed. Otherwise, remove cross-namespace selectors. - Ensure each namespace has distinct labels.
- Consider using network policies to isolate namespaces, but that's separate from scheduling.
Failure Mode 4: Performance degradation due to over-separation
Symptom: Applications experience high latency because dependent pods are on different nodes, forced by anti-affinity.
Recovery:
- Evaluate if anti-affinity is truly needed. For stateful sets where data locality matters, you might use node affinity instead or a mix.
- Use
preferredDuringSchedulingIgnoredDuringExecutionwith a lower weight to allow co-location when necessary. - Monitor metrics (e.g., request latency) before and after changes.
Disaster recovery: rolling back a bad configuration
If a rollout causes widespread issues, you can rollback to a previous revision:
kubectl rollout undo deployment/<name> -n <namespace>
Check rollout status:
kubectl rollout status deployment/<name> -n <namespace>
Documenting recovery
Always have a runbook for critical deployments. Include:
- How to check pod status quickly.
- Safe commands to scale or edit without downtime.
- Rollback procedure.
- Contact for escalation.
Operations Checklist
Use this checklist to maintain secure affinity and anti-affinity configurations over time.
Daily operations
- [ ] Verify that no pods are stuck in Pending due to affinity:
kubectl get pods -A | grep Pending. - [ ] Check cluster node count:
kubectl get nodes(if nodes are removed, required anti-affinity may fail). - [ ] Review events for affinity-related messages:
kubectl get events -A | grep -i affinity.
Weekly audits
- [ ] List all deployments with affinity rules:
kubectl get deployments -A -o json | jq '.items[] | select(.spec.template.spec.affinity != null) | .metadata.name + " in " + .metadata.namespace'. - [ ] Review label selectors for specificity: ensure they include unique app identifiers.
- [ ] Check RBAC permissions for who can modify deployments:
kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 -A5 'deployments'.
Change management
- [ ] Always test affinity changes in a non-production namespace first.
- [ ] Use
kubectl apply --dry-run=serverto validate. - [ ] Stage rollouts: apply changes to one replica or canary before full rollout.
- [ ] Have a rollback plan: note the previous revision before applying (
kubectl rollout history deployment/<name>).
Security hardening reminders
- [ ] Never use sensitive data in labels.
- [ ] Avoid overly broad anti-affinity that can cause self-inflicted denial of service.
- [ ] Combine anti-affinity with network policies for defense in depth.
- [ ] Regularly review node labels and topology keys in use.
Example: checking affinity rules across cluster
Run this command to get a summary of affinity types used:
kubectl get pods -A -o json | jq -r '.items[] | select(.spec.affinity != null) | .metadata.namespace + "/" + .metadata.name + " has affinity"' | sort
This helps you spot unexpected usage.
Conclusion
Kubernetes pod affinity and anti-affinity are powerful tools for controlling pod placement, but they must be configured with security in mind. A misconfigured anti-affinity rule can lead to unschedulable pods and service outages; a permissive affinity rule can undermine isolation and increase risk.
In this guide, we covered:
- Checking your Kubernetes version and environment inventory.
- Applying affinity and anti-affinity rules safely, starting with soft rules and progressing to hard rules.
- Verifying scheduling decisions with
kubectlcommands and understanding event messages. - Diagnosing and recovering from common failures such as pending pods and unintended co-location.
- Following an operations checklist to maintain security and availability.
Next steps:
- Pick one low-risk deployment in a test namespace.
- Add a
preferredDuringSchedulingIgnoredDuringExecutionanti-affinity rule to spread replicas. - Monitor pod placement and note any changes.
- Gradually harden to required rules where appropriate, always having a rollback plan.
Remember that security hardening is iterative. Observe, change one thing, verify, and document. With these practices, you can leverage pod affinity and anti-affinity to improve both resilience and security without sacrificing operational stability.