CDK for Terraform - Infrastructure as actual Code05 Oct 2022
Cloud Development Kits (CDKs) allow you to use a familiar programming language to build Cloud Resources. This means that you can stay in the same IDE and use a familiar syntax to write the code that will run your code.
You don’t have to switch context between different tooling ecosystems. It helps you stay focused.
I use C# as my primary programming language (with Go a close second) and I do a lot of work with Cloud infrastructure. When I work with Terraform, I miss the type safety that a compiled language brings. I miss my refactoring tools to rename, move and safely delete things.
An IDE with a good set of refactoring tools reduces the cost of making code easier to work with.
The main reason by far for using a “proper” language for infrastructure is that I can express infrastructure in a contextual DSL. I’ve written about opinionated Terraform modules and code smells in Terraform. A CDK allows me to do is to have more flexibility in building an opinionated DSL.
By using a CDK, I don’t need to worry about modules. I can work with objects and express re-usability and modularisation with a flexible vocabulary, that I’m familiar with.
Let’s have a look at how this is possible, by looking at a simple example of building the resources necessary for an Azure Function. I’ll be using .Net Core and C# for this example.
Getting started with the CDK for Terraform is easy. I won’t delve into the details of setup too much. You’ll need NPM to get the CDK. The guide to installing the CDK is here
After initialising a new project, I’m good to go. The default project gives me a file to edit. I’m using local state storage for this example.
I use Jetbrains Rider as my IDE.
The first pass
This is my initial attempt to create the resources for the Azure Function app.
This is a simple stack, and I can deploy it via the CLI, after building the project.
I can rely on the compiler to catch any typos that screw up my dependencies. I don’t have to rely on ‘terraform validate’ anymore.
The good thing about using the CDK is that I don’t have to worry about the authentication details. I’ve logged in using the Azure CLI, and as with Terraform, the CDK takes care of the Azure authentication. If I were to use the ARM .Net SDK, I’d have to handle the authentication concerns myself.
There are a few things I don’t like;
- The need to give an id. I know this maps to the resource id in Terraform, but is this needed? Can this be autogenerated? I can see that this is important for backward compatibility to work with the existing state
- The need to pass in the string values of dependencies. Why do I have to pass in the name and the location of the resource group, instead of passing in one resource group object?
- The need to pass in a config object for each resource. I’d assume each resource will be an object by itself
However, I’m ok with this. I can refactor it, as this initial attempt looks a lot like HCL.
Refactor - Extract methods
Next, I extract methods for each resource, into BuildXXX methods.
After extracting methods the orchestration is clear. The dependencies to build the function app in Azure are visible. I use the steps in the excellent book Refactoring to Patterns.
I’ve been running ‘cdktf deploy’ with each refactoring step to make sure the plan doesn’t change.
Using the Builder Pattern
I’m using the Builder Pattern to make sure my Azure Function app resource is built the right way. This is where we can encapsulate ‘opinions’ on how to build the function app, for the domain I’m working in.
If I want to ensure I always want a Linux function app, I can encapsulate that inside the builder.
I start with a Builder for the Azure Function App, because that’s the core resource I want to build. The other resources are dependencies.
I move the BuildXXX methods for each resource, into their own classes.
Let’s take a look at the FunctionBuilder.
I’ve used the methods WithName(), InResourceGroup() and UsingAppServicePlan() to expose configuration that I want the caller to change. Everything else is encapsulated inside the Build() method.
Now my application stack looks like this;
I’ve created builders for the other resources and have a nice set of fluent builders to create my resources.
The dependencies between resources are clear. I can wire up the dependencies with object references. I can hide the complexities of creating a Config object for each resource, and I don’t have to care about resource identifiers because the builder takes care of it.
I could go further and put the builders in a Nuget package and distribute it to the rest of my team (or organisation), and use established versioning practices to share re-usable functionality.
The advantage CDK-TF has over HCL when refactoring is that the plan doesn’t change, something that I’ve found that happens when changing the structure of Terraform in the past.
You don’t have to use the Builder pattern. I’ve used the pattern to demonstrate the flexibility that the CDK brings to the table to create a DSL that works for you. I’ve been able to do all the refactoring with Rider and the compiler makes sure that nothing is broken too much.