Article 18 – Imperative vs Declarative: How to Talk to Kubernetes

The Drift Risk of Step-by-Step Instructions

When managing servers, you typically write scripts that dictate the exact steps to install packages, start services, and edit configurations. This is imperative management.

While simple, it presents challenges at scale. If a step fails, running the script again might cause errors or overwrite configurations.

Additionally, if a user changes a setting manually on the host, your script has no way of detecting that change and restoring the correct state. You have to write complex error-handling steps to manage every state transition.

Describing Desired State and the Three-Way Diff

Kubernetes is designed as a declarative system. Instead of telling the cluster how to perform an action, you describe your desired state in a configuration file and submit it. The Control Plane compares this desired state against the active cluster state and applies the changes needed to align them.

Kubernetes supports three main management models:
1. Imperative Commands: Quick CLI commands (like kubectl run nginx --image=nginx) that update the cluster instantly. While useful for testing, these changes are not documented on disk and cannot be version-controlled.
2. Imperative Object Configuration: You write configuration files but specify the exact action to run (e.g., kubectl create -f pod.yaml or kubectl delete -f pod.yaml). This model is not idempotent. Running create twice on the same file will return an error because the resource already exists.
3. Declarative Object Configuration: You define your configurations in YAML files and apply them using kubectl apply -f pod.yaml.

When you run kubectl apply, Kubernetes performs a Three-Way Merge to calculate the changes:
* The Local Configuration File: The file you just edited (e.g., the new YAML on your laptop).
* The Last-Applied Configuration: Saved as a JSON annotation (kubectl.kubernetes.io/last-applied-configuration) on the live resource in etcd. This tells Kubernetes what the file looked like the last time you ran apply.
* The Live Configuration: The active resource state currently running in the cluster. This includes fields injected by default admissions (like IP addresses or service account tokens) or modified by other controllers.

By comparing these three states, Kubernetes knows:
* If a field was added to your local file, it adds it to the live resource.
* If a field was removed from your local file, and it is present in the last-applied annotation, it deletes it from the live resource.
* If a field is in the live resource but was never in your local file (like an IP address), it ignores it.

This level of intelligence makes kubectl apply fully idempotent, allowing it to serve as the backend engine for GitOps deployment systems.

Reconciling State declaratively

Let’s look at how this difference works in practice when scaling workloads. If you use the imperative method, you scale your deployment directly from the terminal:

$ kubectl scale deployment/my-web-deploy --replicas=5
deployment.apps/my-web-deploy scaled

While this updates the cluster, your local YAML configuration files are now out of sync.

If you use the declarative method, you edit the replicas field inside your configuration file:

spec:
  replicas: 5  # Updated from 3

You then apply the updated file:

$ kubectl apply -f web-deployment.yaml
deployment.apps/my-web-deploy configured

If you execute the apply command again immediately without making changes:

$ kubectl apply -f web-deployment.yaml
deployment.apps/my-web-deploy unchanged

Kubernetes notices that the active configuration matches your desired state and takes no action. Using declarative files allows you to manage your infrastructure using code repositories, tracking history and updates cleanly.