Why we chose Pulumi over Terraform

Nitric Logo
Rak SivRak Siv

Rak Siv

9 min read

Why we chose Pulumi over Terraform

Navigating the vast world of cloud computing can often feel like trying to piece together a jigsaw puzzle with a myriad of complex pieces.

The Nitric framework provides a set of APIs and foundational cloud resources such as queues, buckets, schedules, and events. These tools aim to assist developers in building resilient and scalable applications without needing to navigate each cloud service.

A key feature of the framework is its ability to analyze application code. This analysis results in the automatic generation of specification that details the necessary resources and policies for the application's operation. The goal is to simplify the development process and to align resource allocation more closely with the application's actual needs.

Our team has consistently chosen tools that not only fit our immediate needs but align with our broader company culture and long-term vision. In the case of provisioning infrastructure, we opted for Pulumi. Nitric uses the Pulumi Automation API for its pre-built provider plugins.


Choosing a framework to handle our providers was a fairly straightforward exercise, as one vendor completely aligned with our philosophy of building an open-source enrichment to the developer experience for cloud application builders.

Here are the top reasons why we chose to implement our infrastructure providers with Pulumi over Terraform.

An Alignment with the DevOps Culture

DevOps, at its core, seeks to break down the barriers between development and operations. It emphasizes collaboration, automation, and a shared understanding between the two traditionally siloed teams. Teams working with programming languages will naturally drift away from teams using low code or configuration such as Terraform.

Speaking the same language

Pulumi allows teams to define infrastructure using general-purpose programming languages like Python, TypeScript, Go, and C#. This means that developers and operations can use a programming language that their team is already familiar with for their infrastructure code. At one point we did experiment with providers written using CDKTF, but, found Pulumi providers to have greater usage, support, and stability.

Note: Developers of providers with Nitric are free to use any IaC solution including Terraform.

For the providers built for our open-source framework, we opted for the performance of GO. Using the Pulumi automation engine and GO, our team could easily avoid the nuances of Terraform, we’ll show examples of limitations with examples in the upcoming sections.

Modularity by Default

With modularity as a goal for Nitric’s developer experience, it was a key technological evaluation for our infrastructure provisioning.

Modularity Challenges with Terraform

While building simple components with Terraform can be fairly straightforward, once you get into reuse and modularity there is a lot of repetition of boilerplate code and string substitution.

Let’s illustrate this with a basic description of a cloud-watch event rule in AWS for a single cloud-watch event.

resource "aws_cloudwatch_event_rule" "every_five_minutes" {
  name                = "every-five-minutes"
  description         = "Fires every five minutes"
  schedule_expression = "rate(5 minutes)"

To convert this into a reusable module, we’ll need to separate the configurable parts into input variables and potentially also provide outputs, depending on the requirements.

variable "name" {
  description = "The name of the CloudWatch event rule."
  type        = string
  default     = "every-five-minutes"

variable "description" {
  description = "The description for the CloudWatch event rule."
  type        = string
  default     = "Fires every five minutes"

variable "schedule_expression" {
  description = "The scheduling expression for the CloudWatch event rule."
  type        = string
  default     = "rate(5 minutes)"

resource "aws_cloudwatch_event_rule" "schedule" {
  name                =
  description         = var.description
  schedule_expression = var.schedule_expression

output "rule_arn" {
  description = "The Amazon Resource Name (ARN) of the rule."
  value       = aws_cloudwatch_event_rule.schedule.arn

Once we have our module, we can use this within our main infrastructure code by copying and configuring the following snippet.

module "my_schedule" {
  source              = "./cloudwatch_schedule_module"
  name                = "my-custom-name"
  description         = "My custom description"
  schedule_expression = "rate(10 minutes)"

output "my_schedule_rule_arn" {
  value = module.my_schedule.rule_arn

The limitations of this approach:

  1. Control logic: It lacks the rich logic and control structures (e.g. loops, conditionals) that are present in general-purpose programming languages.

    1. Though recent versions of Terraform have introduced constructs like for_each, general-purpose languages have a broader intuitive set of iteration capabilities making them more appropriate for complex conditions.
  2. Error handling: This might be the biggest miss with Terraform. All errors are runtime errors. Much of the inspection is done after drafting the configuration with inspection tools. Handling errors is straightforward in languages that offer strict typing, exception handling, or detailed error-handling constructs.

  3. Encryption of secrets: Terraform stores its state in plaintext by default. If secrets are managed as regular state items in Terraform, they will be in plaintext in the state file. This is a well-known issue and the general guidance has been to use state backends that encrypt at rest, such as AWS S3 with server-side encryption enabled. Secrets stored in the Pulumi state are never stored in plain text. By default they are encrypted at rest and during transmission and can only be accessed with the encryption key.

  4. Advanced testing: Testing infrastructure code in Terraform isn't as straightforward as unit testing in general-purpose languages. While there are tools like terratest that allow for this, they require learning and integration. Importantly, it is difficult to replicate this style of testing across a variety of projects or project teams.

    Pulumi’s support for standard programming languages means that testing isn't an afterthought. You can employ the same rigorous testing methodologies to your infrastructure as you normally would with your software written in node, java, go, etc.

How Nitric Leverages Pulumi for Modularity

Pulumi infrastructure components are modular by default because they are easily composed into classes or functions which promotes reusability across different projects.

  // Create a new eventbridge schedule
  res.Schedule, err = scheduler.NewSchedule(ctx, name, &scheduler.ScheduleArgs{
    ScheduleExpression:         pulumi.String(awsCronValue),
    ScheduleExpressionTimezone: pulumi.String(args.Tz),
    FlexibleTimeWindow: &scheduler.ScheduleFlexibleTimeWindowArgs{
      Mode: pulumi.String("OFF"),
    Target: &scheduler.ScheduleTargetArgs{
      Arn:     args.Exec.Function.Arn,
      RoleArn: role.Arn,
      Input:   pulumi.Sprintf(scheduleInputTemplate, name),

Nitric’s goal is to elevate the developer experience by applying principles like DRY (Don’t repeat yourself). Each time an application is deployed, Nitric inspects the code to build a resource map. In the following example, our resource map will result in a scheduled task and the associated function. During deployment this resource map is fulfilled by providers that associate the requests with appropriate resources.

// Run every 5 minutes
schedule('process-transactions').every('5 minutes', async (ctx) => {
  console.log(`processing at ${new Date().toLocaleString()}`)

Pulumi's inherent modularity in its infrastructure components offers a robust foundation for efficient code composition and reusability across projects. By leveraging Pulumi, Nitric not only streamlines the developer experience but also aligns operational governance and standardization practices making it a great strategic choice for infrastructure automation.

Open Source Licensing

Terraform's choice of the Business Source License 1.1 might not strike the right chord with everyone. This sentiment is underscored by support for initiatives like Open-Tofu.

The choice of an open-source license is only one component of a project's commitment to the community. Pulumi’s decision to go with the Apache License 2.0 is a testament to its commitment to fostering a free, open community which aligns more closely with Nitric’s core values.

We believe that being a part of an open-source community enriches developer experiences by fostering collaborative learning and innovation. This collective effort drives the simplification of building applications in the cloud, making technology more accessible and efficient for all.

In Summary

Our journey in selecting the right infrastructure as code tool was both analytical and philosophical. We required a solution that not only provided technical excellence but also resonated with our values.

Pulumi's alignment with DevOps culture, modularity for reusability, advanced error handling, and commitment to open-source principles made it an ideal choice for us. We're confident our decision to opt for Pulumi will ensure that our infrastructure remains agile, robust, and in tune with the needs of our development teams.

Previous Post
Build Cloud-native Applications in Go
Next Post
Nitric Update - September 2023