If you follow my blog you'll have noticed I've been playing with Terrafom a lot lately. The main reason is I like the infrastructure-as-code approach and because it solves a couple of problems I experienced with ARM templates. I cannot say Terraform is the holy grail, but a lot better than ARM in my view.
Recently I a colleague pointed me to a new kid on the block: Pulumi.
A big difference between Terraform and Pulumi is the fact that Pulumi really is infrastructure-as-code.
Looking at Terraform, you describe the resources in a JSON definition, which is later interpreted and converted into calls to the Management API. So actually it's not infrastructure-as-code at all, more like infrastructure-as-description.
Both tools track the changes or state around deployments. Terraform uses a ‘tfstate’ file, which you can store locally or for example in an Azure Storage Account. Pulumi wants you to store that data in the Pulumi cloud and that means that when Pulumi is down, you cannot update a deployment anymore. There is however an option to store that data locally, but you need to dive into the FAQ to find it.
Pulumi is aware of the competition out there, so they explicitly mention the differences with Terraform here, which is an interesting read.
The proof of the pudding…..so let's see how easy it is to create an Azure API Management instance, similar to the one I generated with Terraform in my previous blog post.
Converting Terraform to Pulumi
On the page where Pulumi compares itself to Terraform, they also mention a tool which they recommend to convert Terraform to Pulumi.
In our case that would be the easiest approach….if that works…..and….if it generates my favorite C#.
Unfortunately it only generates TypeScript and I really want to have C# as the rest of my projects are also written in that. This means I won't use this tool. Besides the mandatory TypeScript there are some other limitations mentioned, but if you have small Terraform files you might benefit from this.
First step is to install the Pulumi CLI, which we need in any case. Currently the most recent version is 1.14, but 2.0 is in in beta. I guess a big change is comming up. The first thing I notice is that Windows 10 is complaining about the installer not being signed. I don't understand why Pulumi doesn't take the effort to make this a more safe and smooth experience.
Next step is installing the .NET Core 3.0 SDK (or later) which you as Visual Studio developer probably already have installed. Pulumi needs the
dotnet executable in order to build the Pulumi .NET application.
Finally we can start with Pulumi. We create a new project with the
pulumi new azure-csharp CLI command and immediately you're redirected to the Pulumi site to login so Pulumi can store the state file in the cloud.
As I don't want that at this point in time, I cancel the wizard and use the
pulumi login --local command first, so I can setup the state file locally. Then I run the command to create a new project again.
The create wizard asks a couple of questions and when it has all information, it starts downloading the dependencies and creates a C# Console application for you.
Pulumi project structure
The project consists of the files you know from C# console applications. Beside that there are files to contain (envionment) variables and the to-be implementation class (by default called ‘MyStack.cs’ and a ‘stack’ refers to a setup for an environment like dev or stg). I renamed the class to ‘DeploymentStack’, as the environment variables determine the settings for each one of them. If you need real separate deployments per environment, then something like ‘DevelopmentStack’ or ‘DevStack’ would be better.
The generated code, based on the variables provided to the wizard, looks like this:
pulumi up command the project will be initialized, compiled and like Terraform, it will ask you whether you want to make the changes Pulumi has identified. Based on the code in ‘DeploymentStack.cs’ and the configuration settings, it will create a resource group and storage account for us. Make sure to run
az login upfront to make asure the resources end up in the correct Azure subscription.
After this has run, the resources are created and because I insisted to have the state file locally, Pulumi provides me with a link to that file, containing the current state:
While checking the created resources, I found two generated names for the resource group and storage account. Nothing regarding that has been configured so that makes sense, although I would have picked the project name as resource group name instead of some generated value.
Like with Terraform: the state file is holy. When I manually delete something from the resource group and run the ‘up’ command again, the only thing Pulumi checks is the state file and doesn't compare it to the actual situation. So while I deleted some resource manually, running ‘up’ again happily informs me no change is found.
Setting up APIM
Now we have the basics setup, let's start with the actual project of generating an Azure Storage Account, Key Vault, Application Insights and API Management resource with a custom domain, product, user, subscription, custom subscription keys and an example heart beat API.
A lot of examples can be found here, but almost all of them are written in TypeScript. I suppose the collection of C# examples will be extended, but for now I need to do this from scratch.
The order of resources to create is the same for Terraform and Pulumi, so don't expect a lot of difference there. After all there are dependencies between resources, so if you browse through the ‘Stack’ class you'll find a similar approach to Terraform.
In my github repo you can find the code to deploy almost the full set of resources.
To prevent a lengthy (and duplicate) description of the resouces, I invite you to take a look at the repo.
I tried to get as much running as possible, but I couldn't finish a full deployment.
- Uploading a certificate to Key Vault failed, as I don't have a valid pfx (Terraform doesn't complain at all about that)
- Assigning the APIM system identity permissions to Key Vault (could not access apim.Identity.PrincipalId needed to set the access permissions, although the documentation mentions it)
- Custom domain on APIM (because no certificate and not assigned permissions)
While doing research for this blog post I ran into a copule of interesting things I'll discuss in the next section.
In the short amount of time I spend on Pulumi I had a specific goal: to create a APIM setup similar to what I have for Terraform.
This specific goal made me search for specific solutions to specific problems, because APIM is not your average resource. So what is the verdict?
The things I like about Pulumi:
- It's really a great way to write real infrastructure as code
- You can use all programming concepts available to you, which drastically simplifies things like conditional creation and looping over resources
- You don't need to wait until a certain basic feature is implemented, like reading a file from disk in base64, as that's part of the .NET framework already
- Code is compiled, which means early validation and IDE features like code complete and intellisense are available to you
- You can run the validation as part of your build pipeline, so you won't find out as late as in the release step
- Specifically for the APIM part, I like the fact that you can provide a custom subscription key directly, which removes the need for custom Powershell
However, there are some things that can be improved:
- It feels like there is a close relation between HashiCorp Terraform and PulumiCorp Pulumi
- Often in the documentation there is a reference to Terraform, like here, where it says “This content is derived from https://github.com/terraform-providers/terraform-provider-azurerm/blob/master/website/docs/r/api_management.html.markdown"
- At a certain time after running the
upcommand I got this error: “….already exists - to be managed via Terraform this resource needs to be imported into the State….”
- It turns out Pulumi uses Terraform providers under the hood
- Classes representing resources are sometimes less obvious, like “Insights” where ApplicationInight would be better. For APIM the name is “Service”, which is plain unclear
- Classes seem generated (probably also related to the previous point), which leads to strange constructs like “certificatecertificate” and “CertificateCertificatePolicyArgs” object names
- The infrastructure-as-code approach can be further implemented by having enums for values that currently are fixed strings, like app insights type, user state, storage types, tiers, systemassigned, and so on
- Error messages are sometimes unclear. For example when you make a mistake in the variable file, you can get “error: getting secrets manager: yaml: line 10: did not find expected alphabetic or numeric character”. A cryptic message if you ask me. The error was on line 10, although not very obvious.
- There is a default time-out of about 22 minutes for resource creation and for APIM this is way too short. That doesn't need to be an issue, but I got an error saying “context deadline exceeded”. Nothing about this in the documentation and even Google doesn't know. The solution is quite easy in this case, by overriding the standard timeout with a custom value.
The idea of infrastructure-as-code is not new, but Pulumi tries to really implement it. Currently there still are quite some loose ends and I'm not impressed by the documentation so far. Docs are unclear, incomplete and don't cover explanation of error messages and the way to solve them. For example how to solve the time out error, is something that should be in the documentation or FAQ.
There surely is potential but some polishing needs to be done.