Kubernetes · Chapter 2

Kubernetes Resources

Pods, ReplicaSets and Deployments, how they are updated and rolled back, the declarative approach with kubectl apply and organizing with Namespaces.

In the previous chapter we saw the cluster architecture. Now it is time for what gets deployed on it: Kubernetes resources (or objects). We will go through the chain you will use every day, Pod → ReplicaSet → Deployment, how to update an application without downtime and how to organize everything with Namespaces.

Pods: the smallest unit

Kubernetes does not run loose containers: it wraps them in a Pod, the smallest unit that can be created and scheduled on a node.

  • The norm is one container per Pod. To scale, you create more Pods, not more containers inside the same Pod.
  • A Pod can hold several containers when they work together inseparably. For example, a sidecar that collects logs or a service mesh proxy. Those containers share the network (they see each other on localhost) and storage.

Defining a Pod in YAML

Every Kubernetes object is described with four top-level fields:

Field What it is for
apiVersion API version of the object (v1 for Pods).
kind Object type: Pod, ReplicaSet, Deployment...
metadata Name, labels and other data about the object.
spec The desired state: for a Pod, its containers.
apiVersion: v1
kind: Pod
metadata:
  name: mi-pod
  labels:
    app: miapp
    tier: front-end
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - containerPort: 80

Basic commands

# Create a Pod quickly, without YAML (the image must exist in the registry)
kubectl run nginx --image=nginx

# Create or update from the file
kubectl apply -f mi-pod.yaml

# Query
kubectl get pods                 # Pods in the current namespace
kubectl get pods -o wide         # with each Pod's IP and node
kubectl get pods -A              # Pods in every namespace
kubectl describe pod nginx       # Pod details and events

ReplicaSets: several copies, always alive

A single Pod is not resilient: if it dies, nobody replaces it. The ReplicaSet is a controller that guarantees there is always a given number of identical Pods running:

  • High availability: if a Pod goes down, it creates another.
  • Scaling and load distribution: replicas can be spread across several nodes.

Its predecessor was the ReplicationController, now considered legacy. The ReplicaSet does the same and adds a more flexible selector.

Selector and labels: how it knows which Pods to watch

The ReplicaSet does not "remember" the Pods it created: it finds them by their labels. The selector.matchLabels field says which labels the Pods that belong to it must have, and the template defines how to create the missing ones.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: miapp-replicaset
  labels:
    app: miapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: miapp
      tier: front-end
  template:                 # Pod template
    metadata:
      labels:
        app: miapp          # must match the selector
        tier: front-end
    spec:
      containers:
        - name: web
          image: nginx
          ports:
            - containerPort: 80

Two important consequences:

  • A ReplicaSet can adopt Pods that already existed if they carry the selector's labels.
  • If you manually create a Pod with those same labels and the replica count has already been reached, the ReplicaSet deletes it: it never allows more copies than requested.

Scaling a ReplicaSet

kubectl get replicasets            # or: kubectl get rs
kubectl describe rs miapp-replicaset

# Option 1: change replicas in the YAML and apply it
kubectl replace -f miapp-replicaset.yaml

# Option 2: scale from the command line
kubectl scale rs miapp-replicaset --replicas=6

Watch out: kubectl scale changes the object in the cluster, not your YAML file. If you apply the file afterwards, you will go back to the replica count the file says.

Deployments: the usual way to deploy

In practice you almost never create ReplicaSets by hand. You use a Deployment, a higher-level object that creates and manages ReplicaSets for you and adds what a ReplicaSet cannot do: update the application in a controlled way and roll back.

A Deployment manages ReplicaSets, and each ReplicaSet manages Pods

Its YAML is almost identical to the ReplicaSet's; only the kind changes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: miapp-deployment
  labels:
    app: miapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: miapp
  template:
    metadata:
      labels:
        app: miapp
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
kubectl apply -f miapp-deployment.yaml
kubectl get deployments
kubectl get rs          # the ReplicaSet the Deployment created
kubectl get pods
kubectl get all         # all of the above at a glance

Rollouts, revisions and rollback

Every time you change a Deployment's Pod template (for example, the image), Kubernetes starts a rollout and saves a revision. Internally, the Deployment creates a new ReplicaSet with the new version and gradually moves Pods from the old ReplicaSet to the new one.

# Follow progress and view the history
kubectl rollout status deployment/miapp-deployment
kubectl rollout history deployment/miapp-deployment

# Update the image without editing the file
kubectl set image deployment/miapp-deployment web=nginx:1.28

# Go back to the previous revision (or a specific one)
kubectl rollout undo deployment/miapp-deployment
kubectl rollout undo deployment/miapp-deployment --to-revision=1

Since the previous ReplicaSet is kept (with zero replicas), rollback is fast: Kubernetes only has to scale it up again.

About --record: many guides use kubectl apply --record to save the command in the history. That flag is deprecated. Today you annotate the reason for the change like this: kubectl annotate deployment/miapp-deployment kubernetes.io/change-cause="nginx 1.28"

Deployment strategies

Recreate shuts everything down before creating; RollingUpdate replaces Pods gradually

Strategy How it works Downtime?
RollingUpdate (default) Replaces Pods gradually: creates a new one, removes an old one, and so on. No
Recreate Deletes all the old Pods and then creates all the new ones. Yes, while the new ones start

It is chosen in the Deployment's spec.strategy.type.

Imperative vs. declarative

There are two ways to work with Kubernetes:

  • Imperative: you tell the system what to do and how, step by step: create this, change that, delete the other.
  • Declarative: you describe the final state in files and the system works out which changes are needed to get there.
Imperative Declarative
kubectl run, kubectl create, kubectl expose kubectl apply -f file.yaml
kubectl scale, kubectl set image kubectl apply -f folder/
kubectl edit (changes the cluster, not your file) The file is the source of truth
kubectl replace --force -f (deletes and recreates) It can be versioned in Git

Imperative is handy for quick tests and in timed exams. For real environments, declarative is the recommended approach: the file in Git always reflects what is in the cluster.

How kubectl apply works

kubectl apply compares three versions of each object to decide what to change:

  1. Your local file (what you want).
  2. The live object in the cluster (what there is).
  3. The last applied configuration, which Kubernetes stores in JSON as an annotation on the object, under the key kubectl.kubernetes.io/last-applied-configuration.

Thanks to the third one, apply knows when you removed a field from your file and must delete it from the live object, instead of leaving it as it was. If the object does not exist, it simply creates it.

Namespaces: organizing the cluster

A Namespace is an isolated space inside the cluster, with its own resources, permissions and limits. It is used to separate environments (development, testing, production) or teams.

Kubernetes ships with several out of the box:

Namespace What it is used for
default Where resources are created if you do not specify another.
kube-system Kubernetes internal components. Leave it alone.
kube-public Resources readable by all users, even unauthenticated ones.
kube-node-lease Node heartbeats.

Two namespaces in the same cluster and how their services call each other

DNS names across namespaces

Within the same namespace, a service is reached by its name (db-service). To reach one in another namespace you use the full name:

db-service.dev.svc.cluster.local
└─service──┘└ns┘└type┘└─cluster domain─┘

Working with namespaces

# Create
kubectl create namespace dev

# View resources in a specific namespace or in all of them
kubectl get pods --namespace=dev
kubectl get pods --all-namespaces

# Change the default namespace of your context
kubectl config set-context --current --namespace=dev

To always create an object in a namespace, put it in its metadata:

metadata:
  name: mi-pod
  namespace: dev

Limiting resources with ResourceQuota

A ResourceQuota sets the maximum resources a namespace can consume in total:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: cuota-dev
  namespace: dev
spec:
  hard:
    pods: "10"
    requests.cpu: "4"
    requests.memory: 5Gi
    limits.cpu: "10"
    limits.memory: 10Gi

Key takeaways

  • The Pod is the smallest unit; it usually holds a single container.
  • Every object has apiVersion, kind, metadata and spec.
  • The ReplicaSet keeps N copies of a Pod and finds them by labels with its selector.
  • The Deployment manages ReplicaSets and adds rollouts, history and rollback.
  • RollingUpdate (default) causes no downtime; Recreate does.
  • Declarative (kubectl apply) is recommended; apply uses the last-applied-configuration annotation to compute changes.
  • Namespaces separate resources; across them you use the DNS name service.namespace.svc.cluster.local, and a ResourceQuota limits their consumption.

In the next chapter: scheduling, how Kubernetes decides which node each Pod goes to.