There are many good articles available that discuss in depth the difference between imperative and declarative style programming. Here I'm going to take a look at a very narrow subset of this debate, that of Infrastructure as Code, (Iac), and hopefully show off the difference between the two approaches with actual examples rather than high level discussion.
CloudFormation and Terraform are both declarative Infrastructure as Code (Iac) tools. They have a lot of similarities but also quite a few, often subtle, differences. Each has its own advantages and disadvantages but broadly speaking they are both high quality and will do the job well. I'm going to focus the examples on CloudFormation but at its core Terraform has a very similar paradigm so the examples apply equally well.
For the imperative side i'll use the AWS CLI with bash. It's a simple, widely understandable option and most of the principles will be the same as when using a higher level SDK in a language such as Python or Java.
IaC
The core of a declarative style language/tool is that you say what, whereas in an imperative style language you say how. In the case of IaC; for declarative you specify a list of resources you would like, whereas for imperative you specify a list of commands to run to create the resources that you want.
As an example let's begin with an S3 bucket created in a declarative way using CloudFormation and the same S3 bucket created in a imperative way using the AWS CLI. I've chosen the S3 bucket because its a relatively simple, isolated resource that many people will be familiar with.
Declarative: CloudFormation
In a declarative style you simply say what resources you would like, and any properties they should have:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: mybucketname
Imperative: AWS CLI
In an imperative style you specify what commands should be run:
$ aws s3api create-bucket --bucket "mybucketname"
Both of these have the same output when you first run them, a single S3 bucket. On the surface it may seem like the imperative option is simpler, it's definitely fewer lines. However there is very little point writing commands that you can only ever run once. Let's consider what happens if you run your script again. In the case of CloudFormation, it will verify that there is a bucket with the correct settings and return a success message. In the case of the CLI it will return an error because you are asking it to create a bucket that already exists, instead your code will need to look like:
$ BUCKET_NAME="mybucketname"
$ if ! `aws s3api list-buckets --query Buckets[*].Name | grep $BUCKET_NAME > /dev/null`; then aws s3api create-bucket --bucket $BUCKET_NAME; else echo "Bucket already exists, continuing..."; fi
The imperative option is a little bit more complicated now but at least it will handle the case where your bucket already exists so you can keep extending your infrastructure definition.
However consider a more complicated resource such as an EC2 instance, this resource has hundreds of properties, some that dynamically change such as a state
which may be started
, stopped
, starting
, etc. Simply checking there is a resource with the same name is not complete and may hide other problems. In reality you should check that the resource is present and all values are correct, this is what declarative tools do for you, automatically.
Also, any keen eyes will notice that there are bugs in the imperative example already, take for example the case where you already have a bucket called mybucketnameandsomethingelse
. The grep we're using will match that and not create our new bucket. I won't bother fixing for this example but it serves to show how quickly it gets tricky to handle all edge cases properly.
Taking the next step, what happens if you want to alter a property on your bucket? Perhaps add a tag to indicate the owner. Let's have a look again at both options.
Declarative: CloudFormation
Because the declarative resource is just a record of our resources we can simply add or change the values we want on our existing record and CloudFormation will take care of applying any changes. So our script now looks like this:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: mybucketname
Tags:
- Key: "Owner"
Value: "Mike Brooks"
Imperative: AWS CLI
In the case of imperative we need to work out how to apply the tags ourselves:
$ BUCKET_NAME="mybucketname"
$ if ! `aws s3api list-buckets --query Buckets[*].Name | grep $BUCKET_NAME > /dev/null`; then aws s3api create-bucket --bucket $BUCKET_NAME; else echo "Bucket already exists, continuing..."; fi
$ aws s3api put-bucket-tagging --bucket $BUCKET_NAME --tagging 'TagSet=[{Key=Owner,Value="Mike Brooks"}]'
As a final example let's say your team decide that all bucket names should be prefixed with the company name. This will require deleting and recreating the bucket regardless of which tool you're using. For simplicity's sake let's assume the bucket is not yet in use so has no objects.
Declarative: CloudFormation
Simple enough for CloudFormation, just update the value and it will take care of making the change for us:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: companyname-mybucketname
Tags:
- Key: "Owner"
Value: "Mike Brooks"
Imperative: AWS CLI
At first glance it appears simple enough in the case of the cli as well, we can just update the the bucket name variable and rerun our code:
$ BUCKET_NAME="companyname-mybucketname"
$ if ! `aws s3api list-buckets --query Buckets[*].Name | grep $BUCKET_NAME > /dev/null`; then aws s3api create-bucket --bucket $BUCKET_NAME; else echo "Bucket already exists, continuing..."; fi
$ aws s3api put-bucket-tagging --bucket $BUCKET_NAME --tagging 'TagSet=[{Key=Owner,Value="Mike Brooks"}]'
However by doing this in the imperative way we now have an orphaned resource. There is still the old bucket in place with the name mybucketname
, we also have to tell the code how it should make changes, not just the changes we want, this means adding in an explicit clean up step:
$ OLD_BUCKET_NAME="mybucketname"
$ if `aws s3api list-buckets --query Buckets[*].Name | grep $OLD_BUCKET_NAME > /dev/null`; then aws s3api delete-bucket --bucket $OLD_BUCKET_NAME; else echo "Bucket doesnt exist, continuing..."; fi
$
$ BUCKET_NAME="companyname-mybucketname"
$ if ! `aws s3api list-buckets --query Buckets[*].Name | grep $BUCKET_NAME > /dev/null`; then aws s3api create-bucket --bucket $BUCKET_NAME; else echo "Bucket already exists, continuing..."; fi
$ aws s3api put-bucket-tagging --bucket $BUCKET_NAME --tagging 'TagSet=[{Key=Owner,Value="Mike Brooks"}]'
Very quickly we've taken a simple example of an S3 bucket, made a couple of changes and our imperative code is full of logic that needs to be carefully considered and tested for bugs to make sure that your IaC remains reliable and reusable. Given a year or two and several developers building hundreds or thousands of resources, almost all of them, more complicated than this S3 bucket you can quickly see how an imperative approach would become unmanageable.
Teardown
There is one final benefit of the declarative style I wanted to present: teardown. As discussed in my recent post When was the last time you deleted your infrastructure? a regular teardown and create of your infrastructure is an important component of maintaining the accuracy of your IaC.
As our declarative IaC is a list of all the resources we have, most declarative tools maintain a record of the current state of our objects, this means when we want to practice a teardown we can simply tell CloudFormation or Terraform to destroy our resources in a single command and they will go away and do it for us, managing all hierarchy problems.
With an imperative approach we would need to write our own teardown method, likely to be a completely separate script that must be updated every time a resource is added to the create script. It's also entirely on the developer to understand any hierarchy problems, e.g. you cannot delete a VPC until all the EC2 instances have been successfully deleted.
In defence of imperative
This post has been fairly critical of the imperative style of programming. Certainly for Infrastructure as Code cases I find that a declarative style is much more approachable and manageable, for other types of programming this may not be the case. Additionally it's worth remembering that declarative programming has its own issues, ultimately you are handing off a lot of control over how changes are made, this means you lose a large amount of flexibility and normally have to stay on the 'official' path. In the case of IaC this trade-off tends to be a good thing in almost all deployments, though certainly not always.
If you do find yourself needing more control than your declarative tool offers, the first thing I would recommend is ask yourself what you are really trying to achieve, normally if something is complex or awkward to achieve it's non-standard or unusual, so you may be straying down a path you don't need to.
If you do really need the control the suggestion I would offer is to consider a hybrid model, maintain your infrastructure in the declarative CloudFormation or Terraform script and wrap the deployment of those in custom logic to handle any specific requirements. This preserves the bulk of the benefits of declarative infrastructure whilst allowing custom logic.
Summary
In summary declarative infrastructure tools like Terraform and CloudFormation offer a much lower overhead to create powerful infrastructure definitions that can grow to a massive scale with minimal overheads. The complexities of hierarchy, timing, and resource updates are handled by the underlying implementation so you can focus on defining what you want rather than how to do it.
The additional power and control offered by imperative style languages can be a big draw but they also move a lot of the responsibility and effort onto the developer, be careful when choosing to take this approach.