Pulumi vs Terraform. An In-Depth Comparison.

asnocode.com
Source - env0.com.

The world of Infrastructure-as-Code (IaC) has evolved by leaps and bounds. With a growing number of IaC tools available, DevOps teams often find themselves weighing the pros and cons of popular choices that pits Pulumi vs Terraform and others.

In this blog, I’ll take a deep dive into the features, similarities, differences, and real-world use cases of Terraform and Pulumi. Specifically, I’m going to focus on the following:

  • Comparison of Pulumi and Terraform: I’ll cover features, language flexibility, and community support.
  • Languages: Terraform utilizes its own HCL while Pulumi supports multiple common programming languages. I will review how these differences in approach to state management can make one more suitable than the other.
  • Real-world examples: I’ll demonstrate how both tools can be used for managing cloud infrastructure across multiple platforms securely.

So, buckle up as we explore the fascinating world of Pulumi and Terraform.

My Setup

  1. A GitHub account: I’ll use GitHub Codespaces with all necessary tools installed for you.
  2. Repository: TL;DR: You can find the main repo here.

Understanding Infrastructure-as-Code (IaC)

For posterity, remember that Infrastructure-as-Code provisions infra resources using preset configuration files instead of manually. 

The beauty of IaC lies in its ability to treat infrastructure like code, allowing us to manage it with the same processes, tools, and programming languages as application code. 

This enables us to leverage software development practices like version control, testing, and automation throughout the infrastructure provisioning lifecycle, making it easier to allocate infrastructure resources efficiently.

Pulumi vs. Terraform

Terraform and Pulumi are key players in the IaC arena, each offering unique features for infrastructure automation. Terraform shines in its broad compatibility with cloud providers such as AWS, Azure, and GCP

The use of its domain-specific language, Hashicorp Configuration Language (HCL), lets you adopt software engineering practices in managing infrastructure with Terraform. It operates on a declarative model, focusing on what the end state of the infrastructure should look like.

Pulumi, on the other hand, aims to deliver a developer-centric experience by offering support for multiple languages including TypeScript, Go, .NET, Python, and Java. Its architecture is equipped with essentials such as a language host, command-line interface, and state management capabilities.

While Terraform specializes in a focused, declarative approach with its HCL, Pulumi offers broader language support and flexibility, making each tool uniquely suited for various cloud computing tasks and environments.

Key Features Comparison

Terraform stands out with a broad set of features such as its declarative approach, extensive platform support, and a wealth of community modules

It allows users to express infrastructure requirements in a simple yet powerful way and integrates with a diverse selection of plugins and tools created by the community.

Meanwhile, with its user-friendly interface, extensive integrations, and native provider support, Pulumi becomes a compelling choice for developers working with Infrastructure-as-Code.

One of Pulumi’s standout features is its Dynamic Provider Support, which enables the tool to create Terraform providers and support new resources and features at a much faster pace than Terraform. 

This allows Pulumi to stay up-to-date with the latest cloud or SaaS features and resources, ensuring developers can access cutting-edge tools and technologies.

Here is a high-level comparison of the two tools:

PulumiTerraform
Configuration Language Multiple: Python, JavaScript, TypeScript, Go, C#, F#, YAMLHashicorp Configuration Language (HCL)
PluginsCompatible with Terraform providers and has its own Pulumi providersLarge ecosystem of Terraform providers
State ManagementPulumi Cloud hosts state by default, with option to move hosting to another cloud service or manage manuallyState is manually managed manually JSON state files (terraform.tfstate)
State EncryptionEncrypted by defaultUnencrypted by default (premium feature)
TestingUnit, property, and integration testing, also compatible with external testing frameworkIntegration testing, new testing feature as of v1.6.0
IntegrationNative integration for the available config languagesThird-party scripts

Pros & Cons

Another way to look at the two tools is to pit their pros and cons against each other. I do that here in this chart, that dives a little deeper into the contrasts of the previous section.

PulumiTerraform
Pros Multilingual:
Can you standard programming language without learning a new DSL

Strong Typing:
Fewer mistakes, better IDE support

Dynamic Logic:
Loops, conditionals in config

Rich Outputs:
Detailed CLI diffs
HCL:
Dynamically typed DSL that is well known among domain-specific languages

Well-Established:
Larger community and more tools

State Management:
Multiple backends

Extensible:
Provider-based



Cons Learning Curve:
Tough if used to HCL

State Management:
SaaS backend limitations

Community:
Smaller but growing
Language:
HCL is less expressive

Complexity:
Advanced features tricky

Error Messages:
Can be cryptic

Concurrency:
Locking not flexible

When to Choose Terraform

  1. Well-Established Infrastructure: When you're working with traditional VMs, networking configurations, and databases, Terraform has a strong track record here.
  2. Declarative Code: If you prefer infrastructure to be declared in a domain-specific language designed solely for that purpose, Terraform's HCL is your friend.
  3. Multi-Cloud & Provider Ecosystem: Massive selection of providers and modules. Great if you have a heterogeneous environment with multiple clouds or SaaS services.
  4. Community and Resources: Abundance of tutorials, courses, and third-party tools. Also, a very active community.

When to Choose Pulumi

  1. Full Programming Languages: If you want the expressiveness and power of full-fledged languages like Python, TypeScript, or Go, Pulumi lets you code away.
  2. Application-Oriented: Better suited for modern, container-based, or serverless architectures. You can even deploy your app code along with your infrastructure.
  3. Dynamic Configuration: Need complex logic, loops, or conditionals? Pulumi's programming language support makes this a breeze.
  4. Strong Typing and IDE Support: With general-purpose languages, you get the benefits of strong typing and excellent IDE support for auto-completion, error-checking, etc.
  5. Integrated Config Management: Manage your secrets and configs in the same language as your infrastructure code, without requiring separate tools.
  6. Open-Source: If your organization prefers open-source tools for compliance or philosophy, HashiCorp's latest decision to change Terraform's license to BSL might deter you. Pulumi would be a better option.

In specific scenarios, such as multi-cloud infrastructure management, both Terraform and Pulumi offer valuable solutions for uniformly managing resources across different environments.

Licensing and How It May Affect Your Terraform vs. Pulumi Decision

  1. License Flexibility: If you're comfortable with Apache 2.0, Pulumi SDK offers a pretty flexible license for most use cases.
  2. Commercial Features: For enterprise-grade features, you'd likely end up in a paid plan for both Terraform and Pulumi, depending on your needs.
  3. Open-Source Commitment: If an OSI-approved license is a strict requirement, Pulumi SDK meets that criterion.

Let's now compare Pulumi and Terraform with this simple example to create an S3 bucket in AWS.

Terraform AWS S3 Bucket Example

Terraform Files

Below is our main.tf file.

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "5.19.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "terraform-bucket" {
  bucket = "env0-terraform-example"

  tags = {
    Name        = "My env0 Terraform Example Bucket"
    Environment = "Dev"
    Team        = "Engineering"
  }
}


resource "aws_s3_bucket_ownership_controls" "terraform-bucket" {
  bucket = aws_s3_bucket.terraform-bucket.id
  rule {
    object_ownership = "BucketOwnerPreferred"
  }
}

resource "aws_s3_bucket_acl" "terraform-bucket" {
  depends_on = [aws_s3_bucket_ownership_controls.terraform-bucket]

  bucket = aws_s3_bucket.terraform-bucket.id
  acl    = "private"
}


Run Terraform

Now all I need to do is run the following commands:

# Set AWS Access Key ID as an environment variable to authenticate with AWS.
# Replace 'your-access-key-id' with your actual AWS Access Key ID.
export AWS_ACCESS_KEY_ID=your-access-key-id

# Set AWS Secret Access Key as an environment variable for AWS authentication.
# Replace 'your-secret-access-key' with your actual AWS Secret Access Key.
export AWS_SECRET_ACCESS_KEY=your-secret-access-key

# Initialize Terraform project. This sets up the backend, downloads necessary providers, etc.
terraform init

# Run a Terraform plan to preview the changes that will be made to the infrastructure.
# This step is a dry-run that shows what will happen when you actually apply the changes.
terraform plan

# Apply the planned changes to the infrastructure.
# This step will actually create, update, or delete resources to match the configuration.
# It will show a preview of these actions and ask for confirmation before proceeding.
terraform apply

and here is the output of running Terraform:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.terraform-bucket will be created
  + resource "aws_s3_bucket" "terraform-bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "env0-terraform-example"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "Environment" = "Dev"
          + "Name"        = "My env0 Terraform Example Bucket"
        }
      + tags_all                    = {
          + "Environment" = "Dev"
          + "Name"        = "My env0 Terraform Example Bucket"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)
    }

  # aws_s3_bucket_acl.terraform-bucket will be created
  + resource "aws_s3_bucket_acl" "terraform-bucket" {
      + acl    = "private"
      + bucket = (known after apply)
      + id     = (known after apply)
    }

  # aws_s3_bucket_ownership_controls.terraform-bucket will be created
  + resource "aws_s3_bucket_ownership_controls" "terraform-bucket" {
      + bucket = (known after apply)
      + id     = (known after apply)

      + rule {
          + object_ownership = "BucketOwnerPreferred"
        }
    }

Plan: 3 to add, 0 to change, 0 to destroy.
aws_s3_bucket.terraform-bucket: Creating...
aws_s3_bucket.terraform-bucket: Still creating... [10s elapsed]
aws_s3_bucket.terraform-bucket: Creation complete after 16s [id=env0-terraform-example]
aws_s3_bucket_ownership_controls.terraform-bucket: Creating...
aws_s3_bucket_ownership_controls.terraform-bucket: Creation complete after 1s [id=env0-terraform-example]
aws_s3_bucket_acl.terraform-bucket: Creating...
aws_s3_bucket_acl.terraform-bucket: Creation complete after 0s [id=env0-terraform-example,private]

Cleanup with Terraform

To go ahead and clean up, run terraform destroy.

Pulumi AWS S3 Bucket Example

Since you can use different programming languages with Pulumi, I'm most comfortable with Python, so we'll go with that.

To get started with Pulumi, I can use the command pulumi new. Here's what happens when you run pulumi new:

  1. Template Selection: Pulumi will first ask you to pick a template – AWS Python stack, an Azure TypeScript stack, or one of a number of others. You can even use your own custom template if you have one. For this tutorial, I’m using AWS Python.
  2. Project Metadata: You'll be prompted to fill in some metadata for your new project, like the project name, stack name, and sometimes config values that the template requires. This sets up the pulumi.yaml and pulumi.[stack-name].yaml files.
  3. File Generation: Pulumi will generate a set of files based on the template you chose. These usually include a pulumi.yaml file for the project metadata and source files in the language of your choice (e.g., __main__.py for Python and for our example).
  4. Install Dependencies: If the template has any dependencies (like AWS SDK for a Python AWS template), Pulumi will install them for you.
  5. Ready to Go: After all this, you'll have a new Pulumi project directory all set up and ready for you to start coding your infrastructure.

I can then cd into our project directory and run pulumi up to deploy our new stack, but first, I have to edit the generated code in __main__.py to tailor it to our specific needs.

I've taken a few screenshots of this process below.

Selecting our aws-python template
Answering a few prompts and installing dependencies
Our project is ready

Pulumi Files

These are the files that were created in the previous process.

pulumi.s3-pulumi-test.yaml

config:
  aws:region: us-east-1

pulumi.yaml

name: PulumiTest
runtime:
  name: python
  options:
    virtualenv: venv
description: Testing Pulumi with Python for S3

requirements.txt

pulumi>=3.0.0,<4.0.0
pulumi-aws>=6.0.2,<7.0.0

This is the original __main__.py file

"""An AWS Python Pulumi program"""

import pulumi
from pulumi_aws import s3

# Create an AWS resource (S3 Bucket)
bucket = s3.Bucket('my-bucket')

# Export the name of the bucket
pulumi.export('bucket_name', bucket.id)

And below is our updated __main__.py file

import pulumi
from pulumi_aws import s3

# Create an AWS resource (S3 Bucket)
bucket = s3.Bucket("env0-pulumi-example",
                   bucket="env0-pulumi-example",
                   acl="private",
                   tags={
                       "Name": "My env0 Pulumi Example Bucket",
                       "Environment": "Dev",
                   })

ownership = s3.BucketOwnershipControls(
    "bucket-controls",
    bucket=bucket.id,
    rule={"object_ownership": "BucketOwnerPreferred"}
)

# Export the name of the bucket
pulumi.export("bucketName", bucket.id)

Run Pulumi

Let's now run Pulumi, but first, you'll need to log in to Pulumi since we're going to use the Pulumi SaaS to store our stack. Make sure you have a Pulumi account then run pulumi login. Create a token and feed it in the prompt. Now you're ready to run the following commands:

pulumi preview
Previewing update (s3-pulumi-test)

View in Browser (Ctrl+O): https://app.pulumi.com/samgabrail/PulumiTest/s3-pulumi-test/previews/bc3ff0f6-4e84-4a80-9d64-aed495c97f4f

     Type                               Name                       Plan       
 +   pulumi:pulumi:Stack                PulumiTest-s3-pulumi-test  create     
 +   ├─ aws:s3:Bucket                   env0-pulumi-example        create     
 +   └─ aws:s3:BucketOwnershipControls  bucket-controls            create     


Outputs:
    bucketName: output[string]

Resources:
    + 3 to create

and when satisfied run pulumi up and choose 'yes' when prompted.

pulumi up
Previewing update (s3-pulumi-test)

View in Browser (Ctrl+O): https://app.pulumi.com/samgabrail/PulumiTest/s3-pulumi-test/previews/68f6a538-b4e8-419f-95bf-6b5198a2706e

     Type                               Name                       Plan       
 +   pulumi:pulumi:Stack                PulumiTest-s3-pulumi-test  create     
 +   ├─ aws:s3:Bucket                   env0-pulumi-example        create     
 +   └─ aws:s3:BucketOwnershipControls  bucket-controls            create     


Outputs:
    bucketName: output[string]

Resources:
    + 3 to create

Do you want to perform this update? yes
Updating (s3-pulumi-test)

View in Browser (Ctrl+O): https://app.pulumi.com/samgabrail/PulumiTest/s3-pulumi-test/updates/1

     Type                               Name                       Status              
 +   pulumi:pulumi:Stack                PulumiTest-s3-pulumi-test  created (3s)        
 +   ├─ aws:s3:Bucket                   env0-pulumi-example        created (0.83s)     
 +   └─ aws:s3:BucketOwnershipControls  bucket-controls            created (0.66s)     


Outputs:
    bucketName: "env0-pulumi-example"

Resources:
    + 3 created

Duration: 5s

Cleanup with Pulumi

Now go ahead and run pulumi destroy to clean up and delete the bucket.

Summary

In conclusion, both Terraform and Pulumi offer powerful features and benefits for managing and deploying infrastructure. While Terraform’s maturity and extensive community support make it a popular choice, Pulumi’s developer-friendly approach and growing ecosystem position it as an increasingly attractive alternative. 

By considering factors such as language support, state management, community resources, and specific use cases, organizations can make an informed decision between these two powerful IaC tools to best meet their infrastructure management needs. Better yet, use both with env0 since it's a framework-agnostic platform.


  Tags:   pulumi terraform iac cloud

Share on