Imperative vs. declarative Kubernetes commands: What's the difference?

Declarative and imperative configuration commands

One of the interesting features of the Kubernetes container orchestration technology is that it’s state-based.

Under Kubernetes, once you define how the various resources within a cluster of virtual or physical machines are supposed to be configured, Kubernetes ensures that configuration is always in force.

Under Kubernetes is etcd, a database that keeps track of the state of the given Kubernetes cluster. If you declare a pod deployment of three replicas, Kubernetes guarantees that three pods are always running. If you declare a namespace with the name coolspace, Kubernetes ensures that the namespace is always there.

Ensuring state is one of the reasons that Kubernetes is a compelling technology.

Imperative and declarative compared

There are two ways you can configure a resource under Kubernetes: imperatively or declaratively.

Imperative configuration means that to describe the configuration of the resource, you execute a command from a terminal’s command prompt. Of course, the terminal’s machine must have permission to access the Kubernetes cluster of interest.

Apache and Docker Tutorials

Master the fundamentals of Apache, Docker and Kubernetes.

Declarative configuration means that you create a file that describes the configuration for the particular resource and then apply the content of the file to the Kubernetes cluster. To apply the configuration, you use the command kubectl apply at the command prompt as you would for an imperative command, but that’s where the similarity ends.

Let’s look at some examples of imperative and declarative configuration for commonly used Kubernetes resources: pods, deployments, namespaces and services.

Imperative K8s pod configuration

The first example we’ll look at is creating a pod. To create a pod imperatively, execute the kubectl run command set in a terminal window:

kubectl run nicepod --image=nginx

Execute the command set kubectl get pods, and you’ll see the following result which indicates the pod has been created and is running.

NAME      READY   STATUS    RESTARTS   AGE
nicepod   1/1     Running   0          26s

To create a pod declaratively, you create a manifest file, either in JSON or YAML. Once the file is created, execute the command set kubectl apply -f <FILENAME> from the command prompt.

The following example is a manifest file in YAML, named nicepod.yaml, that creates a pod with the name nicepod.

apiVersion: v1
kind: Pod
metadata:
  name: nicepod
  labels:
    App: dev
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP

Kubernetes resource creation

To create the source, run the following command in a terminal window:

kubectl apply -f nicepod.yaml

The command above essentially says, “Apply this declaration defined in the file nicepod.yaml to create the resource within the Kubernetes cluster.”

Once the YAML file is applied, the resource is created. To verify all is as intended, execute the command kubectl get pods. You’ll see the following output:

NAME      READY   STATUS    RESTARTS   AGE
nicepod   1/1     Running   0          50s

As you can see, both imperative and declarative configuration produce the same outcome.

Configuring Kubernetes deployments

Let’s do an imperative configuration of a Kubernetes deployment.

A deployment is a Kubernetes resource that is a collection of one or many pods. A deployment is guaranteed to always be running. Creation of a Kubernetes deployment requires execution of two imperative commands. The first command, shown below, creates the deployment named cooldeploy which is a collection of pods that run the Nginx web server:

kubectl create deploy cooldeploy --image nginx

The second imperative command scales the deployment up to three replicas of the pod:

kubectl scale --replicas=3 deployment/cooldeploy

Thus, deployment using the command kubectl get deployment cooldeploy generates the following output:

NAME         READY   UP-TO-DATE   AVAILABLE   AGE
cooldeploy   3/3     3            3           85s

To create the same deployment declaratively, construct a manifest file in YAML named mydeployment.yaml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cooldeploy
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

Next, run the command kubectl apply -f mydeployment.yaml to apply the resource definition to the Kubernetes cluster. Then, run the command kubectl get deployment cooldeploy, and you get output similar to the following:

NAME         READY   UP-TO-DATE   AVAILABLE   AGE
cooldeploy   3/3     3            3           85s

As with the Kubernetes resource creation, the result is the same regardless of whether you take an imperative approach or declarative approach.

Imperative K8s namespace configuration

Let’s configure a Kubernetes namespace imperatively. Execute the command kubectl create namespace coolspace in a terminal window on a machine that’s configured to access a Kubernetes cluster.

Voila! You’ve just created a namespace. Run the command kubectl get ns to see the result. You’ll see the following output:

NAME              STATUS   AGE
coolspace         Active   11s
default           Active   39m
kube-node-lease   Active   39m
kube-public       Active   39m
kube-system       Active   39m

Notice that the namespace we defined, coolspace, is now one of the namespaces in this particular Kubernetes cluster.

Declarative K8s namespace configuration

To create the namespace declaratively, create a YAML file named coolspace.yaml as shown below:

kind: Namespace
apiVersion: v1
metadata:
  name: coolspace
  labels:
    name: dev

Then execute the command kubectl apply -f coolspace.yaml to create the namespace. The command kubectl get ns returns output similar to the response we got when doing the imperative configuration:

NAME              STATUS   AGE
coolspace         Active   7s
default           Active   43m
kube-node-lease   Active   43m
kube-public       Active   43m
kube-system       Active   43m

Imperative service configuration

Finally, let’s configure a Kubernetes service.

Implementing a Kubernetes service is a bit more complex because a service has many configuration parameters. Take a look at the following imperative configuration:

kubectl expose deployment cooldeploy --port=8080 --target-port=80 --type=LoadBalancer

The kubectl expose command set creates a Kubernetes service that exposes the deployment named cooldeploy. Specifically, the service exposes port 8080 to the public by using a service type, LoadBalancer, and the public port 8080 is bound to the pods running internally on port 80 in deployment.

Thus, when we run the imperative declaration shown above, we get the following output:

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
cooldeploy   LoadBalancer   10.101.215.194   172.17.0.79   8080:32402/TCP   52s

Notice the name of the service is the same as the name of the deployment, cooldeploy. Also, that service is exposed on the public URL 172.17.0.79:8080. With an imperative approach such as the one shown above the service is named by default. Nevertheless, the service does work. In this case, when you CURL the IP address and port, you get output from one of the web servers in the deployment as represented by the service:

curl 172.17.0.79:8080

The above CURL command generates the following output:

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to nginx!</title>
    <style>
    body {
      width: 35em;
      margin: 0 auto;
      font-family: Tahoma, Verdana, Arial, sans-serif;
  }
</style>
...

Now, let’s configure the service declaratively. Create a YAML file named myservice.yaml as shown below:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
    port: 8080
    targetPort: 80
  type: LoadBalancer

Apply the service to the Kubernetes cluster like so:

kubectl apply -f myservice-yaml

Then you’ll get the following output:

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
my-service   LoadBalancer   10.97.48.73      172.17.0.79   8080:30798/TCP   23s

Notice that the service named my-service is up and running in the cluster. Where did the name come from? Look at the manifest file listed above. The file defines the service declaratively. Notice the name attribute underneath the attribute metadata. That’s where the name of the service is defined.

Declarative vs imperative: Which one should I choose?

Imperative and declarative configuration are two distinctly different approaches to create Kubernetes resources. Both techniques are useful, but they have tradeoffs.

The most important tradeoff is one of security. Under the right conditions, creating resources by executing imperative commands can be a security nightmare. For starters,  there is no audit trail. Once an imperative command executes, the state of the cluster changes. Without significant access control precautions within the Kubernetes cluster, anybody can alter the cluster and the resulting change is not apparent.

The declarative approach has an intrinsic level of security, because you need a manifest file to alter the state of the cluster. Companies usually store manifest files within a source control management system (SCM) like Git or GitHub. This provides a rudimentary audit trail. At the least, if something goes haywire in the cluster, admins can go to the SCM and see the latest manifest file that altered the state of the cluster to figure out what happened

That said, imperative configuration of a Kubernetes cluster is useful for developers and system administrators that want to experiment. You don’t need to go through all the details of creating and maintaining manifests files to implement elementary designs. You simply create the resources you need, when you need them.

Imperative configuration involves creating Kubernetes resources directly at the command line against a Kubernetes cluster. Declarative configuration defines resources within manifest files and then applies those definitions to the cluster. Once you understand the benefits and tradeoffs to the imperative and declarative approaches, you can start to understand the best approach given the situation at hand.