How Pulumi Compares to Terraform for Infrastructure as Code

How Pulumi Compares to Terraform for Infrastructure as Code

📅 21 December, 2018 – Kyle Galbraith

I have been a huge fan of Terraform for a lot of my recent work. There is something about the modularity it brings to infrastructure as code that I really like. If you haven’t checked out my earlier posts around Terraform give them a read.

While Terraform is a great tool, it’s always worth a little bit of time to explore other options out there. Getting pigeonholed or stuck with one tool, language, or framework is can be a recipe for disaster in most instances. It’s worth knowing what other options are out there and the advantages as well as the disadvantages to using them. One that caught my attention a few months back in the infrastructure as code space is Pulumi.

What’s interesting about Pulumi is that it removes one of the biggest barriers to entry around Infrastructure-as-Code, another language to learn. Unlike Terraform there isn’t another domain-specific-language (DSL) to learn. If you know JavaScript, TypeScript, Python, or Go then you can start codifying your infrastructure right out of the box.

In this post, I am going to give a brief introduction to Pulumi and how to get started with it. Then we’re going to use Pulumi to set up a static website bucket and CloudFront distribution in AWS, a use case I have used in other Terraform examples. After that, we can take a look at some advantages and disadvantages between the two.

Getting started with Pulumi

As is the case with most things tech related, we have to install Pulumi first. If your running macOS we can grab this directly from brew.

$ brew install pulumi

A quick sanity check of the version should show that we got everything installed.

$ pulumi version

Got a version printed out? Great, let’s move to the next step.

Like Terraform, Pulumi supports multiple cloud providers including AWS, Azure, Google Cloud, Open Stack, Kubernetes, and even their own cloud framework.

For this post, we are going to use Pulumi with AWS. If you don’t already have the AWS CLI installed and configured, you should do that now. By default Pulumi is going to use the credentials you create when you configure the AWS CLI via aws configure.

Using Pulumi with Amazon Web Services

To get started we are going to create a barebones Pulumi project using the command below.

$ pulumi new aws-javascript --dir pulumi-static

This will first prompt you to login to Pulumi, go ahead and hit enter to have the login open in your browser. Once logged in you can pop back over to the terminal and finish the setup.

  1. Leave the project name as the default.
  2. Leave the project description as the default.
  3. Create the -dev stack as well.
  4. Select no when prompted with the create option.

Now we should be able to look at the contents of our pulumi-static folder and see that we have two Pulumi YAML files, an index.js, and of course our package files.

If we were to open up pulumi.io and login we would also see that we have a project, pulumi-static with two stacks in it. We can also verify this on our command line by listing our stacks.

$ pulumi stack ls

Now let’s dive into our index.js file and provision some AWS infrastructure. When we first open up our file we see it already has some code inside of it. Let’s pause for a second and talk about what this code is currently doing.

"use strict";
const pulumi = require("@pulumi/pulumi");
const aws = require("@pulumi/aws");

const bucket = new aws.s3.Bucket("my-bucket");

exports.bucketName = bucket.bucketDomainName;

This is code representing some AWS infrastructure. More specifically we see that it creates a new S3 bucket named my-bucket and then the name of that bucket is exported. Notice that this is just traditional JavaScript, no need to learn another domain-specific language.

We can go ahead and run this code from the command line to provision our AWS infrastructure. First, we run npm install and then we can run our up command.

$ npm install
$ pulumi up

Select pulumi-static-dev as the stack we want to provision.

Once we have selected our stack we are presented with a plan of what resources will be created, updated, or destroyed. Much like the terraform plan command.

Previewing update (pulumi-static-dev):

     Type                 Name                             Plan       
 +   pulumi:pulumi:Stack  pulumi-static-pulumi-static-dev  create     
 +   └─ aws:s3:Bucket     my-bucket                        create     
 
Resources:
    + 2 to create

Select yes on the confirmation prompt to create the bucket. This will create our S3 bucket based on the code we just looked at. Once our stack is created we should see the following logs from Pulumi.

Updating (pulumi-static-dev):

     Type                 Name                             Status      
 +   pulumi:pulumi:Stack  pulumi-static-pulumi-static-dev  created     
 +   └─ aws:s3:Bucket     my-bucket                        created     
 
Outputs:
    bucketName: "my-bucket-ed42950.s3.amazonaws.com"

Resources:
    + 2 created

Awesome! We provisioned our first piece of AWS infrastructure using Pulumi. Let’s modify our infrastructure to not only create an S3 bucket but to enable static website hosting on that bucket.

Using Pulumi to provision our static website infrastructure

To enable S3 website hosting for our bucket we need to add an additional property as well as a bucket policy that allows for public reads. This is very easy to accomplish in Pulumi as well. Here is what our code looks like after adding website hosting to our bucket.

"use strict";
const pulumi = require("@pulumi/pulumi");
const aws = require("@pulumi/aws");

const websiteBucket = new aws.s3.Bucket("my-bucket", {
  website: {
    indexDocument: "index.html"
  }
});

let bucketPolicy = new aws.s3.BucketPolicy("publicReadPolicy", {
  bucket: websiteBucket.bucket,
  policy: websiteBucket.bucket.apply(publicReadPolicyForBucket)
});

exports.websiteUrl = siteBucket.websiteEndpoint;

function publicReadPolicyForBucket(bucketName) {
  return JSON.stringify({
    Version: "2012-10-17",
    Statement: [
      {
        Effect: "Allow",
        Principal: "*",
        Action: ["s3:GetObject"],
        Resource: [
          `arn:aws:s3:::${bucketName}/*` // policy refers to bucket name explicitly
        ]
      }
    ]
  });
}

Here we add a website config property to the bucket we are creating. We then define a BucketPolicy that is attached to our website bucket. The policy allows anybody to perform s3:GetObject on our bucket, which is necessary for static website hosting out of an S3 bucket.

We can apply these changes to our stack by running another up command on our command line.

$ pulumi up

We should see that our bucket policy is going to be created and that our current bucket is going to be updated.

Previewing update (pulumi-static-dev):

     Type                    Name                             Plan       Info
     pulumi:pulumi:Stack     pulumi-static-pulumi-static-dev             
 ~   ├─ aws:s3:Bucket        my-bucket                        update     [diff: +website]
 +   └─ aws:s3:BucketPolicy  publicReadPolicy                 create     
 
Resources:
    + 1 to create
    ~ 1 to update
    2 changes. 1 unchanged

Once we perform the update we should see that our S3 static website hosting url is outputted.

Outputs:
  - bucketName: "my-bucket-e4e2278.s3.amazonaws.com"
  + websiteUrl: "my-bucket-e4e2278.s3-website-us-west-2.amazonaws.com"

Resources:
    + 1 created
    ~ 1 updated
    2 changes. 1 unchanged

Awesome! We now have an S3 bucket that is configured to host a static website and it was all provisioned via Pulumi using JavaScript.

Let’s extend this one more time by adding a CloudFront distribution that sits in front of our S3 website bucket. This requires us to create a new distribution and set our S3 bucket as an origin. Here is what our code looks like after adding the CloudFront CDN to our infrastructure.

"use strict";
const pulumi = require("@pulumi/pulumi");
const aws = require("@pulumi/aws");

const websiteBucket = new aws.s3.Bucket("my-bucket", {
  website: {
    indexDocument: "index.html"
  }
});

let bucketPolicy = new aws.s3.BucketPolicy("publicReadPolicy", {
  bucket: websiteBucket.bucket,
  policy: websiteBucket.bucket.apply(publicReadPolicyForBucket)
});

const cloudFrontDistribution = new aws.cloudfront.Distribution("myBucketDistribution", {
    enabled: true,
    defaultRootObject: "index.html",
    origins: [
        {
            customOriginConfig: {
                httpPort: 80,
                httpsPort: 443,
                originProtocolPolicy: "match-viewer",
                originSslProtocols: ["TLSv1", "SSLv3"]
            },
            originId: websiteBucket.websiteEndpoint,
            domainName: websiteBucket.websiteEndpoint
        }
    ],
    defaultCacheBehavior: {
        viewerProtocolPolicy: "redirect-to-https",
        allowedMethods: ["GET", "HEAD", "OPTIONS"],
        cachedMethods: ["GET", "HEAD", "OPTIONS"],
        forwardedValues: {
            cookies: { forward: "none" },
            queryString: false,
        },
        targetOriginId: websiteBucket.websiteEndpoint
    },
    restrictions: {
        geoRestriction: {
            locations: [],
            restrictionType: "none"
        }
    },
    viewerCertificate: {
        cloudfrontDefaultCertificate: true
    }
});

exports.websiteUrl = cloudFrontDistribution.domainName;

function publicReadPolicyForBucket(bucketName) {
  return JSON.stringify({
    Version: "2012-10-17",
    Statement: [
      {
        Effect: "Allow",
        Principal: "*",
        Action: ["s3:GetObject"],
        Resource: [
          `arn:aws:s3:::${bucketName}/*` // policy refers to bucket name explicitly
        ]
      }
    ]
  });
}

The key thing we added here is the variable cloudFrontDistribution. It defines all the pieces for our CDN distribution. This includes the default cache behavior, origin for our S3 website bucket, as well as the default restrictions and SSL certificate for our end users.

Another run of pulumi up should provision our new CloudFront distribution.

$ pulumi up
Previewing update (pulumi-static-dev):

     Type                            Name                             Plan       
     pulumi:pulumi:Stack             pulumi-static-pulumi-static-dev             
 +   └─ aws:cloudfront:Distribution  myBucketDistribution             create     
 
Resources:
    + 1 to create
    3 unchanged

Once we have confirmed the update we should see that our CloudFront domain name is outputted for us.

Updating (pulumi-static-dev):

     Type                            Name                             Status      
     pulumi:pulumi:Stack             pulumi-static-pulumi-static-dev              
 +   └─ aws:cloudfront:Distribution  myBucketDistribution             created     
 
Outputs:
  ~ websiteUrl: "my-bucket-e4e2278.s3-website-us-west-2.amazonaws.com" => "d2stkileejh34y.cloudfront.net"

Resources:
    + 1 created
    3 unchanged

Just like that, we have provisioned the infrastructure to support a static website in AWS. We have an S3 bucket for hosting and a CloudFront distribution for our content delivery network. All of this was configured via JavaScript that Pulumi runs to provision our infrastructure.

Pretty cool right? We were able to write code that we know and love to provision the infrastructure we need in Amazon Web Services. We could continue extending this code to add DNS aliases for our distribution and even upload our static website files.

To close things out let’s look back at some of the similarities, differences, advantages, and disadvantages associated with Pulumi as compared to Terraform.

Reflecting on Pulumi

It’s worth noting that Pulumi is a different tool than Terraform and quite frankly there is always room for more tools. Each tool that enters the infrastructure as code space provides different ideas and opinions. We may agree with some of them and disagree with others, that is totally normal. Use what works best for your workflow.

That said, here are some thoughts I had in using Pulumi.

  • Writing your infrastructure in the same programming languages you use on a daily basis is awesome! Avoiding the learning curve associated with yet another DSL is a huge value add. Furthermore, by representing our infrastructure in code like JavaScript we could easily add tests using frameworks we already know and love. The testing piece is something that has bugged many developers using Terraform, there are solutions there as well but not as familiar as testing JavaScript code.
  • plan commands are awesome when it comes to provisioning infrastructure. It is great to see that Pulumi and Terraform have plans in common. It is very helpful to see a visualization of what is going to be created, updated, and destroyed when you are provisioning infrastructure via code.
  • Logging in to use a command line tool is odd. With Terraform we can install the CLI and immediately use it. That’s not the case with Pulumi and I find that to be disruptive.
  • State, state, state. Terraform and Pulumi keeps track of your infrastructure via state files of some kind. This is how they know what to add, update, and remove. The difference between the two is that Pulumi by default stores your state in a web backend (thus the login step mentioned above). Terraform on the other hand stores your state locally by default.
  • Document required fields. This really drove me nuts trying to provision a CloudFront distribution using Pulumi. I actually had to open their source code to see what values they were expecting when creating a new distribution. Terraform has this category nailed in my opinion, everything is documented in one place and tells me exactly what the required properties are.
  • Don’t mix my chocolate with your CloudFormation. The point of tools like Terraform and Pulumi is so that I can use something slightly friendlier than CloudFormation. Nothing against CF, but it’s a mental leap at times that id rather not make. With Pulumi the lack of documentation meant I needed to look up what CloudFormation values are used to provision things like a CloudFront distribution. This is specific to infrastructure inside of AWS so your mileage may vary.

These are my thoughts from using Pulumi for a few weeks. Would I choose to use it for a new project over Terraform? Not likely because I have already ramped up on HCL in Terraform so I don’t have the learning curve some might have to climb. I also find the documentation around Terraform to be better and that greatly expedites things in my development environment.

Pulumi is a very capable tool and offers a lot of the benefits that Terraform does, including writing code in languages you already know and love. But, it still has some rough spots that could use some polish.

Try it out for yourself, build something using it and see if the advantages outweigh any potential disadvantages. Knowing more than one tool never hurt anyone! As always feel free to ask questions or share your own experiences below.

Are you interested in my other projects?

Well, thank you for your interest! I recently created a course focused on learning Amazon Web Services by actually using it. We focus on learning over six different AWS services by actually using them to solve the problem of hosting, securing, and delivering static websites. Grab any package today at 35% off.

I also curate a weekly newsletter, Learn By Doing, that delivers awesome cloud, coding, and tooling content to your inbox every week. Sign up to get it in your inbox every week.

As you read I also just launched parler.io a service that automatically turns your written content into audio that can be downloaded and embedded into other platforms. Try it out for yourself and let me know your thoughts.