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.