What is meant by declarative and imperative?
In programming, there are different ways to give instructions to a computer to achieve the programmer’s desired result. These ways of telling the computer what to do are known as programming paradigms.
In general, they refer to how we build programs from logical ideas such as if
statements or loops. There are other classifications as well: functional, structured, object-oriented, and so on. Each of these describes a different kind of task that programmers might perform when writing code or thinking about code.
Imperative and declarative programming is the most fundamental way in which programmers think about defining their tasks and the two main ways in which we need to think about how we write and structure our Infrastructure as Code.
Before we discuss each way, let us define a quick Infrastructure-as-Code project.
Basic Infrastructure-as-Code project
The following diagram shows the basic infrastructure for deploying a single virtual machine in Microsoft Azure:
Figure 1.1 – The basic Infrastructure-as-Code project diagram
As you can see, the project is made up of the following components:
- Resource group (
rg-iac-example-uks-001
) – This is a logical container in Azure to store the resources. - Virtual network (
vnet-iac-example-uks-001
) – This is a virtual network that will host our example virtual machine. - Subnet (
snet-iac-example-uks-001
) – This is not shown in the diagram, but the virtual network contains a single subnet. - Network Security group (
nsg-iac-example-uks-001
) – As we don’t want management ports such as3389
(RDP) or22
(SSH) open to anyone on the internet, this will add some basic rules to only accept traffic on these ports from trusted sources. This will be attached to the subnet, so the rules apply to all resources deployed there. - Virtual machine (
vm-iac-example-uks-001
) – This, as you may have guessed, is the virtual machine itself. - Network interface (
nic-iac-example-uks-001
) – Here, we have the network interface, which will be attached to the virtual machine and the subnet within the virtual network. - Public IP address (
pip-iac-example-uks-001
) – Finally, we have the public IP address; this is going to be attached to the network interface, which will allow us to route to the virtual machine from the trusted locations defined in the network security group.
While this is a basic infrastructure example, there are quite a few different resources involved in the deployment. Also, as we are going to be talking at a very high level about how this could be deployed, we won’t be going into too much detail on Azure itself just yet, as this will be covered in Chapter 4, Deploying to Microsoft Azure.
Declarative approach
When talking about my own experiences, I mentioned that I used a configuration tool; in my case, this was Puppet. Puppet uses declarative language to define the target configuration – be it a software stack or infrastructure – but what does that mean?
Rather than try and give an explanation, let’s jump straight in and describe how a declarative tool would deploy our infrastructure.
In its most basic configuration, a declarative tool only cares about the end state and not necessarily how it gets to that point. This means the tool, unless it is told to be, isn’t resource-aware, meaning that when the tool is executed, it decides the order in which the resources are going to be deployed.
For our example, let us assume that the tool uses the following order to deploy our resources:
- Virtual network
- Resource group
- Network security group
- Subnet
- Public IP address
- Virtual machine
- Network interface
On the face of it, that doesn’t look too bad; let us explore how this ordering affects the deployment of our resources in Azure.
The following figure shows the results of the deployments:
Figure 1.2 – The results of deploying our infrastructure using a declarative tool
As you can see, it took three deployments for all the resources to be successfully deployed, so why was that?
- Deployment 1: The virtual network failed to be deployed as it needed to be placed with the resource group, which wasn’t deployed yet. As all the remaining resources had a dependency on the virtual network, they also failed, meaning the only successful resource to be deployed during the first execution was the resource group, as that had no dependencies.
- Deployment 2: As we had the resource group in place from Deployment 1, then this time around, the virtual network and subnet both deployed; however, because the deployment of the network security group was attempted before the subnet was successfully deployed, that failed. The remaining failed resources – the public IP address and virtual machine – both failed because the network interface hadn’t been created yet.
- Deployment 3: With the final set of dependencies in place from Deployment 2, the remaining resources – the network security group, public IP address, and virtual machine – all launched successfully, which finally left us with our desired end state.
The term for this is eventual consistency, as our desired end state is eventually deployed after several executions.
In some cases, the failures during the initial deployment of the resources don’t really matter too much as our desired end state is eventually reached – however, with infrastructure, and depending on your target cloud environment, that may not always be true.
In the early days of Infrastructure as Code, this was quite a large issue as you had to build logic to consider dependencies for the resources you were deploying – which not only meant that you had to know what the dependencies were but the bigger your deployment, the more inefficient it became.
This is because the more logic you start adding to the code, the more you start working against the declarative nature of the tool, which also carries the risk of introducing race conditions when the code is executed. For example, if you have one resource that takes five minutes to deploy – how do you know it’s ready? This would mean even more logic, which, if you got it wrong or something unexpected happened, you could be sat waiting for the execution to eventually time out.
Fear not; things have most definitely improved as the development of the tools has matured, and the tools have become more resource-aware. A lot of the manual logic you had to employ is now unnecessary, but there are still some considerations that we will go into in more detail in Chapter 2, Ansible and Terraform beyond the Documentation.
Imperative approach
As you may have already guessed, when using an imperative approach, the tasks execute in the order you define them – and we know the order in which we need to run the tasks to deploy our resources, which is as follows:
- Resource group
- Virtual network
- Subnet
- Network security group
- Network interface
- Public IP address
- Virtual machine
It means that the result of running our first deployment will look like the following:
Figure 1.3 – The results of deploying our infrastructure using an imperative tool
Great, you may be thinking to yourself, it works the first time! Well, sort of; there is a big assumption that you know the order in which your resources need to be deployed, and you need to structure the code in such a way that takes that into account.
So, while this way would typically work first when executed, there could potentially be a little more upfront work to get the scripts in the right order using a little trial and error – however, once they are in the correct order, you can be confident that each time you execute them, they will work the first time.
Now that we have discussed the key differences between declarative and imperative when it comes to Infrastructure as Code, let’s now talk about the differences between another deployment approach, pets versus cattle.