
I will make no secret in that Terraform has become largely a default way of deploying environments into AWS at this point in time, with the reason being a mix of what was available at the time (I started playing with Terraform when it was 0.10.0) and also the ever mythical “cross-cloud” framework rendering more Cloud Provider native solutions like not quite as tenable (althought its safe to say at this point the reality of a “Cloud Agnostic” solution is more based in reality now, i.e that you are doing it for the same tooling rather than somehow making your deployment able to exist across providers).
So, given the slowdown towards and after the Christmas period, I decided to have a bit of a poke at some of the other options that are available. For the extent of this I wanted to look at something I already had some components ready for, namely a small AWS Connect deployment that deploys the bare basics. As part of this I wanted to:
- See how easily I could translate things across
- Not rely on any extra documentation/guides
- Roughly approximate each other (I could have made them more exactly 1-to-1 but at the end of the day after getting the fundamentals together it was more just spending time looking for syntax in documentation rather than in-depth learning)
As a result someone may look at this and say “why would you do it this way?”. The answer is straightforward, it is what the official documentation recommended.
The code that resulted can be found on my Github, so rather than going through all the minor details I wanted to bring my primary thoughts to the fore.
The Shared Notes
The first obvious thing is that both of the tested options use a proper, fully fledged programming language. Rather than the bespoke HCL, we can use regular Python to do whatever tasks we require, meaning there is no restrictions on things like generating names, since if we can write a function we can use it. This also leads to the first flip-side, as it assumes the reader knows and can read/write code to actually make use of this. Generally, this is not a major, however in cases where team members are perhaps not as technically aligned this is harder than teaching a JSON like structure (and there can be debates either way if those people should be touching Infrastructure anyway).
Similar to Terraform both contain the capability to add Tags across the deployed resources, which is great from a cost reconciliation and management perspective, although in this scenario I personally found the way Pulumi defines these a bit cleaner, but not doubt there is a bit more depth to this that can be achieved either way.
Both using Python leads to an interesting foil, and I could potentially see it raising issues in some environments, as it means you might be stuck with specific Python version until large scale rollouts of upgrades happen, and then if that occurs your development environment may then start encountering issues. A small concern in the grand scheme of things, but it is in there for considering.
AWS CDK

At first glance the CDK seemed to be much more overwhelming, and in going through this the initial opinion seemed largely correct. Whilst pretty standard in terms of Pythonic configuration, it seemed a weird mix of having class definition and resources mixed and matched within, although it is noted that this more comes into its own when you start doing larger scale deployments with multiple stacks.
Recognizing that at the end of the day the resources are simply CloudFormation resources helps a bit in this regard, as whilst you can read the documentation specific for CDK sometimes it was easier to go and find the resource definition in CloudFormation to see what it was entailing.

One thing that I did find a bit weird was having to set resource policies for various resources. In general, I can understand why a resource like a Connect Instance may be useful to orphan instead of destroying by default, but instead it leads to a weird situation whilst you are iterating and trying the build-destroy loop to find out that something was left behind in a previous run and needs manual rectification.
On a positive note the ability to define tests for the behaviour of the stack is a wonderful little addition, and could really be handy as things start to get large to ensure specific combinations are creating expected outcomes, and wasn’t something I expected overall. Certainly its one quirk that you will run into with Terraform, especially if a field is calculated, so a plan can sometimes succeed then a deploy fails for something that feels like it should have been able to warn you during an earlier phase.
The weirdest thing I found was deploying a solution via the CLI feels cumbersome, as having to list each parameter with a flag and then set it leads to quite verbose commands, even in a small deployment means a lot of copy pasting. Its one of those pains that I have no doubts has work arounds, but it feels quite clunky relative to things like Terraform or Pulumi where you can use a config file to keep things simple (there may be a way of achieving similar with CDK, but the documentation didn’t seem to point to it when I was reading them).
Overall, I quite liked the CDK, and can see why its quite popular. Especially not having to worry about state is pretty nice, and the fact you can use the code to instead generate a normal CloudFormation template rather than having to deploy via CDK does give it some alternate usage paths, with the obvious downside is you are then stuck with AWS specific tooling (Note: I have also messed with CDKTF which helps work around some of the AWS specificity, but since it is yet to reach GA I wasn’t considering it for this piece, however it does allow you to have a lot of the functionality of the CDK whilst still being usable across providers).
Pulumi
First things first, I should not some previous colleagues really champion Pulumi, so I was curious about what it was about. Certainly giving recent movements by Hashicorp around licencing of Terraform have made Pulumi look a bit more enticing, so I was genuinely excited to jump into it.
The initial experience is…rough. You are FORCED to login to use the CLI, which doesn’t feel great particularly when you just want to quickly try something out. Fortunately you can set it to local, but relative to Terraform or CDK that you just need to set the relative details such as AWS profile to run feels like a blocker that doesn’t need to really be there. The official guide also wasn’t the most forthcoming on how to locally login, making it seem like you need to use a Pulumi account to achieve your outcome, which kind of sucks.
After that, however, things got better. Walking through the initiation (its interesting even a blank password requires an “unlock” if set, but that’s minor), and with the initiation done you can quickly jump into it. The file structure quickly drew my eye for two reasons
- By default you are encourage to write your definition in
__main__.py, which just feels weird more than anything else - Within the file the default structure is similar to Terraform in that its all top level.
These are things that no doubt disappear in more advanced scenarios, but it is interesting the documentation encourages this. After this writing configurations was straightforward coming from Terraform; if you know the relationship you just define and link the resources by the appropriate fields. What did catch me out however was in resource like the encryption configuration needing 3 nested specific configurations to define a resource. This just felt cumbersome (it does assist with type hinting of course, but it doesn’t feel as smooth).

Beyond this there weren’t really any surprises, as writing the configuration went exactly as expected, and then defining the config was straightforward either on the CLI or in the appropriate config file. In particular, its nice to see the ignoreTags block included, since policy can introduce automatic tags.
One thing that did come out from this testing however was how good the output was in pointing out issues and what the resolution would be. Compared to the CDK which by default just triggers a full rollback without much information, this was in line with Terraform whilst still being verbose on the corrective action required.
Pulumi Conversion
One extra thing that I tried whilst doing this was getting Pulumi to convert the existing Terraform into Pulumi, and I will be honest it surprised me how decent it worked. The code itself it generated uses some interesting tricks on conditional logic to achieve outcomes, however reviewing it, it seemed reasonably logical what was happening and why. Genuinely impressed, and it is nice there is a potential offramp should Terraform need to be changed for whatever reason.
Conclusion
The great news is none of these options felt terrible to use, and as someone who programs in Python they felt pretty reasonable in their approaches. I have a few minor quibbles, like the pulumi required login, but I will definitely be trying to upskill on these and keep them in my back pocket for future pieces of work. Certainly, the only major things I missed was the simplicity of defining resources in HCL, and the ease of split Terraform configurations in a directory by arbitrary file name. All of these no doubt have language specific options for achieving in similar ways, but it didn’t seem like the “Getting Started” guides really touched on this.
Neither option took massive amounts of time, and once reaching the coding component both took similar times, just requiring looking at the specific documentation to achieve the needed outcome, so there isn’t a major differentiator there.