Run it in the public monorepo

This course is built around the public HelixWorks Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.

Source: terraform/main.tf

resource "kind_cluster" "helixworks" {
  name            = var.cluster_name
  node_image      = var.kind_node_image
  wait_for_ready  = true
  kubeconfig_path = local.kubeconfig_path

  kind_config {
    kind        = "Cluster"
    api_version = "kind.x-k8s.io/v1alpha4"

Code to reality

Declared intent
Create the local Kubernetes substrate from versioned infrastructure code.
Interpreter
Terraform builds a dependency graph and the Kind provider calls the Docker and Kind APIs.
Software effect
Terraform records a cluster resource and Kind writes a kubeconfig for its Kubernetes API.
Hardware effect
Docker starts containers that consume the laptop resources used as control-plane and worker nodes.
Observable evidence
terraform state show and kubectl get nodes must agree that the declared cluster is ready.

Key terms

TermMeaning
Node affinityAttract pods to nodes by label rules
required...DuringSchedulingHard rule — the pod must match
preferred...DuringSchedulingSoft rule — best effort
weightPriority given to a preferred rule
OperatorMatch logic: In/NotIn/Exists/etc.
nodeSelectorSimpler label-only node targeting

Problem & solution

nodeSelector only does exact-match and can't express "prefer" versus "require". You need richer rules for steering pods toward the right nodes without over-constraining them.

Solution: Use node affinity rules to attract pods to nodes with matching labels (e.g. disktype=ssd), as required or preferred.

The analogy

A refrigerated cargo ship tells the berth-assignment clerk what kind of dock it needs. If it merely prefers a refrigerated berth, the clerk tries for one but will wave it to any open berth when none is free. If it strictly requires a refrigerated berth, the clerk makes it wait offshore until a cold dock frees up. In Kubernetes the clerk is the scheduler, the ship is a pod with node affinity, a preference is a preferred rule, and a hard requirement is a required rule matched against node labels.

Where this fits in the cluster

Node affinity is a pod rule the scheduler evaluates against node labels. It pulls a pod toward matching nodes (the opposite of Day 14's taints).

What is Node Affinity?

A richer, more expressive way for a pod to choose which nodes it can run on, based on node labels. It's the upgrade to the simple nodeSelector.

   nodeSelector  -> exact match only (disktype=ssd)
   nodeAffinity  -> operators (In, NotIn, Exists...), AND required vs preferred

Attract vs repel (vs Day 14)

Keep the mental model straight: taints push pods away, whereas node affinity pulls a pod toward the nodes it prefers.

   Taint/Toleration -> NODE repels pods (permission to land)
   Node Affinity    -> POD is drawn to matching nodes (preference/requirement)

The two flavors

Node affinity comes in a hard form (must match or stay Pending) and a soft form (prefer a match, but run anywhere if none exists).

   requiredDuringSchedulingIgnoredDuringExecution
     = HARD rule. No matching node -> pod stays Pending.

   preferredDuringSchedulingIgnoredDuringExecution
     = SOFT rule. Try to match; if none, schedule anywhere.

   "IgnoredDuringExecution" = once running, label changes won't evict the pod.

The difference comes down to one thing: what the scheduler does when no node matches.

                match node?         no match?
   required:    schedule there      Pending (never runs)
   preferred:   prefer there        runs elsewhere anyway

Required (hard) example

A required rule is a hard constraint — if no node matches the expression, the pod never schedules.

apiVersion: v1
kind: Pod
metadata:
  name: postgres
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: disktype
                operator: In
                values: ["ssd"]
  containers:
    - name: postgres
      image: postgres:16
      ports: [{ containerPort: 5432 }]

Preferred (soft) example with weight

A preferred rule is best-effort; each preference carries a weight so the scheduler can rank competing soft rules.

  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 1                 # 1-100; higher = stronger preference
          preference:
            matchExpressions:
              - key: zone
                operator: In
                values: ["us-east-1a"]

Operators

Match expressions support several operators for comparing a node label against your criteria.

   In        label value is in the list
   NotIn     label value is NOT in the list  (anti-affinity-ish)
   Exists    label key exists (any value)
   DoesNotExist  label key absent
   Gt / Lt   greater/less than (numeric)

Setup: label your nodes

Affinity matches on node labels, so first tag the nodes you want to target.

kubectl label node cka-worker disktype=ssd
kubectl get nodes --show-labels
kubectl get nodes -l disktype=ssd

Verify scheduling

After applying the pod, confirm where it landed and inspect events to explain any Pending state.

kubectl apply -f postgres.yaml
kubectl get pod postgres -o wide       # which node did it land on?
kubectl describe pod postgres          # events explain Pending if no match

nodeSelector vs nodeAffinity

Side by side, nodeAffinity is the more expressive successor to the simple nodeSelector map.

nodeSelectornodeAffinity
Syntaxsimple mapexpressions + operators
Hard/softhard onlyrequired (hard) + preferred (soft)
Powerexact matchIn/NotIn/Exists/Gt/Lt

Best practice: combine with taints

To truly dedicate a node, pair taints/tolerations with node affinity so only your pod can land there and it is forced to do so.

   Taint node (repel everyone) + Toleration (let my pod in) +
   Node Affinity (force my pod TO that node) = truly dedicated node.

End-to-end example: pin to SSD, prefer a zone

A pod that must run on SSD nodes (hard rule) and prefers zone us-east-1a (soft rule). It stays Pending if no SSD node exists.

Graph legend — pinning a real Postgres pod to SSD, preferring a zone:

Graph nodeMaps toWhat it does
postgres - requires disktype=ssd, prefers zone us-east-1athe pod's nodeAffinity (required + preferred)A postgres:16 pod that must run on SSD, prefers one zone
node-1 disktype=ssd zone=us-east-1aa node with both labelsBest match: satisfies required and preferred
node-2 disktype=ssd zone=us-east-1ba node with only disktype=ssdEligible (required only), lower preference
node-3 disktype=hdda node lacking the SSD labelExcluded by the hard required rule
apiVersion: v1
kind: Pod
metadata: { name: postgres }
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:   # HARD
        nodeSelectorTerms:
          - matchExpressions:
              - { key: disktype, operator: In, values: ["ssd"] }
      preferredDuringSchedulingIgnoredDuringExecution:  # SOFT
        - weight: 80
          preference:
            matchExpressions:
              - { key: zone, operator: In, values: ["us-east-1a"] }
  containers:
    - name: postgres
      image: postgres:16
      ports: [{ containerPort: 5432 }]
kubectl label node node-1 disktype=ssd zone=us-east-1a
kubectl label node node-2 disktype=ssd zone=us-east-1b
kubectl apply -f postgres.yaml
kubectl get pod postgres -o wide         # expect node-1 (best match)
kubectl describe pod postgres            # events explain Pending if no SSD node

End-to-end flow

Affinity pulls a pod toward labeled nodes: hard required rules filter first, then soft preferred rules rank the survivors.

Graph legend — hard rules filter, then soft rules rank for Postgres:

Graph nodeMaps toWhat it does
postgres pod with node affinitythe pod's affinity.nodeAffinityDeclares required + preferred placement
required disktype=ssd matches?requiredDuringSchedulingIgnoredDuringExecutionHard filter; no SSD node means Pending
eligible SSD nodesthe surviving candidate nodesNodes that pass the hard rule
preferred zone=us-east-1a matches?preferredDuringSchedulingIgnoredDuringExecution (weight 80)Ranks survivors; runs elsewhere if none match

Key takeaways

  • Node affinity = pod chooses nodes by labels, richer than nodeSelector.
  • required = hard (Pending if unmet); preferred = soft (best-effort).
  • Combine taints/tolerations + affinity to dedicate nodes properly.

Checklist

  • [ ] Labeled nodes and used a required nodeAffinity rule
  • [ ] Saw a pod go Pending when no node matched
  • [ ] Used a preferred rule with a weight
  • [ ] Compared nodeSelector vs nodeAffinity