Mark one or more nodes as schedulable to allow new pods
Mark nodes as schedulable to allow new pods after maintenance. Use this to return cordoned nodes to service following upgrades, troubleshooting, or repairs.
/plugin marketplace add kcns008/cluster-code/plugin install kcns008-cluster-core-plugins-cluster-core@kcns008/cluster-codeMark nodes as schedulable to allow new pods to be scheduled after maintenance or troubleshooting.
Uncordoning returns nodes to normal scheduling operation:
Typical maintenance workflow:
What uncordoning does:
unschedulable flag from nodeWhat uncordoning does NOT do:
kubectl configured with cluster accessnodes/getnodes/updateNODE_INPUT="${NODE}"
LABEL_SELECTOR="${LABEL_SELECTOR}"
VERIFY_HEALTH="${VERIFY_HEALTH:-true}"
echo "đ Identifying nodes to uncordon..."
if [[ -n "$LABEL_SELECTOR" ]]; then
# Select nodes by label
echo " Using label selector: $LABEL_SELECTOR"
NODES=$(kubectl get nodes -l "$LABEL_SELECTOR" -o jsonpath='{.items[*].metadata.name}')
if [[ -z "$NODES" ]]; then
echo "â No nodes found matching selector: $LABEL_SELECTOR"
exit 1
fi
NODE_COUNT=$(echo "$NODES" | wc -w)
echo " Found $NODE_COUNT nodes:"
echo "$NODES" | tr ' ' '\n' | sed 's/^/ - /'
elif [[ "$NODE_INPUT" == *"*"* ]]; then
# Wildcard pattern matching
PATTERN=$(echo "$NODE_INPUT" | sed 's/\*/.*/')
echo " Using pattern: $NODE_INPUT"
NODES=$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | grep -E "^${PATTERN}$")
if [[ -z "$NODES" ]]; then
echo "â No nodes found matching pattern: $NODE_INPUT"
exit 1
fi
NODE_COUNT=$(echo "$NODES" | wc -l)
echo " Found $NODE_COUNT nodes:"
echo "$NODES" | sed 's/^/ - /'
else
# Single node name
NODES="$NODE_INPUT"
# Verify node exists
if ! kubectl get node "$NODES" &>/dev/null; then
echo "â Node not found: $NODES"
echo ""
echo "Available nodes:"
kubectl get nodes -o custom-columns=NAME:.metadata.name,STATUS:.status.conditions[-1].type,SCHEDULABLE:.spec.unschedulable
exit 1
fi
NODE_COUNT=1
echo " Target node: $NODES"
fi
echo ""
if [[ "$VERIFY_HEALTH" == "true" ]]; then
echo "đĨ Verifying node health..."
echo ""
UNHEALTHY_NODES=""
WARNINGS=0
for NODE_NAME in $NODES; do
echo "Checking: $NODE_NAME"
NODE_INFO=$(kubectl get node "$NODE_NAME" -o json)
# Check Ready status
READY_STATUS=$(echo "$NODE_INFO" | jq -r '.status.conditions[] | select(.type=="Ready") | .status')
READY_REASON=$(echo "$NODE_INFO" | jq -r '.status.conditions[] | select(.type=="Ready") | .reason')
if [[ "$READY_STATUS" != "True" ]]; then
echo " â ī¸ Node not Ready (Status: $READY_STATUS, Reason: $READY_REASON)"
UNHEALTHY_NODES="$UNHEALTHY_NODES $NODE_NAME"
WARNINGS=$((WARNINGS + 1))
else
echo " â
Node is Ready"
fi
# Check for pressure conditions
MEMORY_PRESSURE=$(echo "$NODE_INFO" | jq -r '.status.conditions[] | select(.type=="MemoryPressure") | .status')
DISK_PRESSURE=$(echo "$NODE_INFO" | jq -r '.status.conditions[] | select(.type=="DiskPressure") | .status')
PID_PRESSURE=$(echo "$NODE_INFO" | jq -r '.status.conditions[] | select(.type=="PIDPressure") | .status')
if [[ "$MEMORY_PRESSURE" == "True" ]]; then
echo " â ī¸ Memory Pressure detected"
WARNINGS=$((WARNINGS + 1))
fi
if [[ "$DISK_PRESSURE" == "True" ]]; then
echo " â ī¸ Disk Pressure detected"
WARNINGS=$((WARNINGS + 1))
fi
if [[ "$PID_PRESSURE" == "True" ]]; then
echo " â ī¸ PID Pressure detected"
WARNINGS=$((WARNINGS + 1))
fi
# Check kubelet version
KUBELET_VERSION=$(echo "$NODE_INFO" | jq -r '.status.nodeInfo.kubeletVersion')
echo " Kubelet: $KUBELET_VERSION"
# Check if already schedulable
SCHEDULABLE=$(echo "$NODE_INFO" | jq -r '.spec.unschedulable // false')
if [[ "$SCHEDULABLE" == "false" ]]; then
echo " âšī¸ Node is already schedulable"
else
echo " Status: Cordoned (will be uncordoned)"
fi
echo ""
done
if [[ $WARNINGS -gt 0 ]]; then
echo "â ī¸ Found $WARNINGS health warnings"
echo ""
if [[ -n "$UNHEALTHY_NODES" ]]; then
echo "Nodes not in Ready state:$UNHEALTHY_NODES"
echo ""
echo "Continue with uncordon anyway? (yes/no)"
read -r CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "â Uncordon cancelled"
exit 1
fi
fi
else
echo "â
All nodes are healthy"
fi
else
echo "â ī¸ Health check skipped (--verify-health=false)"
fi
echo ""
echo "đ Uncordoning nodes..."
echo ""
SUCCESS_COUNT=0
ALREADY_SCHEDULABLE=0
FAILED_COUNT=0
for NODE_NAME in $NODES; do
# Check if already schedulable
SCHEDULABLE=$(kubectl get node "$NODE_NAME" -o jsonpath='{.spec.unschedulable}' 2>/dev/null)
if [[ "$SCHEDULABLE" != "true" ]]; then
echo " âšī¸ $NODE_NAME - already schedulable"
ALREADY_SCHEDULABLE=$((ALREADY_SCHEDULABLE + 1))
continue
fi
# Uncordon the node
if kubectl uncordon "$NODE_NAME" &>/dev/null; then
echo " â
$NODE_NAME - uncordoned"
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
else
echo " â $NODE_NAME - failed to uncordon"
FAILED_COUNT=$((FAILED_COUNT + 1))
fi
done
echo ""
echo "Results:"
echo " Successfully uncordoned: $SUCCESS_COUNT"
if [[ $ALREADY_SCHEDULABLE -gt 0 ]]; then
echo " Already schedulable: $ALREADY_SCHEDULABLE"
fi
if [[ $FAILED_COUNT -gt 0 ]]; then
echo " Failed: $FAILED_COUNT"
fi
echo ""
echo "đ Verifying uncordon status..."
echo ""
kubectl get nodes $(echo $NODES | tr '\n' ' ') -o custom-columns=\
NAME:.metadata.name,\
STATUS:.status.conditions[-1].type,\
SCHEDULABLE:.spec.unschedulable,\
PODS:.status.allocatable.pods,\
ROLES:.metadata.labels.node-role\\.kubernetes\\.io/*
echo ""
echo "đ Cluster Scheduling Capacity:"
echo ""
TOTAL_NODES=$(kubectl get nodes --no-headers | wc -l)
SCHEDULABLE_NODES=$(kubectl get nodes -o json | jq '[.items[] | select(.spec.unschedulable != true)] | length')
UNSCHEDULABLE_NODES=$((TOTAL_NODES - SCHEDULABLE_NODES))
echo " Total nodes: $TOTAL_NODES"
echo " Schedulable: $SCHEDULABLE_NODES"
echo " Unschedulable (cordoned): $UNSCHEDULABLE_NODES"
# Calculate percentage
SCHEDULABLE_PCT=$(( SCHEDULABLE_NODES * 100 / TOTAL_NODES ))
echo " Schedulable capacity: ${SCHEDULABLE_PCT}%"
if [[ $SCHEDULABLE_PCT -eq 100 ]]; then
echo " â
All nodes are schedulable"
fi
echo ""
echo "đ Monitoring new pod placement..."
echo ""
# Check for pending pods that might now be scheduled
PENDING_PODS=$(kubectl get pods --all-namespaces --field-selector status.phase=Pending --no-headers 2>/dev/null | wc -l)
if [[ $PENDING_PODS -gt 0 ]]; then
echo "âšī¸ $PENDING_PODS pods are currently Pending"
echo ""
echo "Pending pods may now be scheduled to uncordoned nodes:"
kubectl get pods --all-namespaces --field-selector status.phase=Pending -o custom-columns=\
NAMESPACE:.metadata.namespace,\
NAME:.metadata.name,\
AGE:.metadata.creationTimestamp | head -10
echo ""
echo "Monitor scheduling with:"
echo " kubectl get pods --all-namespaces -w"
else
echo "â
No pending pods"
fi
echo ""
echo "â
UNCORDON COMPLETE"
echo "===================="
echo ""
echo "Uncordoned $SUCCESS_COUNT node(s)"
echo ""
echo "Node(s) are now available for scheduling new pods."
echo ""
echo "Next steps:"
echo ""
echo "1. Monitor pod distribution across nodes:"
echo " kubectl get pods --all-namespaces -o wide | grep '$(echo $NODES | tr ' ' '\\|')'"
echo ""
echo "2. Check node resource utilization:"
echo " kubectl top nodes $(echo $NODES | tr '\n' ' ')"
echo ""
echo "3. Verify cluster autoscaler behavior (if enabled):"
echo " kubectl get pods -n kube-system -l app=cluster-autoscaler"
echo ""
echo "4. Monitor node events:"
for NODE_NAME in $NODES; do
echo " kubectl get events --field-selector involvedObject.name=$NODE_NAME"
done
echo ""
echo "Note: Pods that were evicted during drain will NOT automatically"
echo "return to these nodes. New pods or rescheduled pods will use the"
echo "uncordoned nodes based on scheduler decisions."
echo ""
If you want to force pod redistribution:
# Option 1: Scale deployment to trigger new pod placement
kubectl scale deployment <name> --replicas=0
kubectl scale deployment <name> --replicas=<original-count>
# Option 2: Restart deployment (rolling restart)
kubectl rollout restart deployment <name>
# Option 3: Use descheduler (if installed)
kubectl create job --from=cronjob/descheduler manual-deschedule
# Node maintenance completed
cluster-code node-uncordon --node worker-01
# Verify ready for workloads
kubectl get node worker-01
kubectl top node worker-01
# Uncordon all nodes in pool
cluster-code node-uncordon --selector node-pool=workers-v2
# Monitor pod distribution
watch kubectl get pods -o wide
# Quickly uncordon all nodes
cluster-code node-uncordon --node "*"
# Check for pending pods
kubectl get pods -A --field-selector status.phase=Pending
cluster-code node-uncordon --node ip-10-0-1-42.ec2.internal
cluster-code node-uncordon --node "gke-prod-cluster-pool-1-*"
cluster-code node-uncordon --selector eks.amazonaws.com/nodegroup=workers-spot
cluster-code node-uncordon --node worker-05 --verify-health=false
# Find all cordoned nodes
CORDONED=$(kubectl get nodes -o json | jq -r '.items[] | select(.spec.unschedulable==true) | .metadata.name')
# Uncordon them
for node in $CORDONED; do
cluster-code node-uncordon --node $node
done
Uncordoning a node doesn't override pod affinity rules. Pods will only schedule if:
If node has taints, only pods with matching tolerations can schedule, even after uncordon.
Scheduler only places pods if node has sufficient:
If cluster autoscaler is enabled:
Check node conditions:
kubectl describe node <node-name>
Check node resources:
kubectl top node <node-name>
Check pod requirements:
kubectl describe pod <pod-name>
# Look for nodeSelector, affinity, tolerations
Check kubelet logs:
# For systemd-based systems
journalctl -u kubelet -f
# For containerized kubelet
kubectl logs -n kube-system <kubelet-pod>
Check node events:
kubectl get events --field-selector involvedObject.name=<node-name>
node-cordon: Mark node as unschedulablenode-drain: Safely evict pods from nodecluster-diagnose: Analyze cluster healthkubectl describe node: View detailed node statuskubectl top nodes: View node resource usage