Terraform vs Pulumi: Which IaC tool is right for you?

10 min read

Choosing the right Infrastructure as Code (IaC) tool is crucial. In this post, we delve into two popular tools Terraform and Pulumi by deploying the same application to AWS using both, with the help of Nitric. We’ll explore key features, differences, and strengths of each tool to help you make an informed decision.

Prefer a video version? Check it out below:

The setup I used

Why Use Nitric for This Comparison?

Nitric offers a unique advantage by allowing developers to swap out providers without altering application code. This flexibility ensures a consistent infrastructure across different tools, making it ideal for comparing Terraform and Pulumi. Nitric integrates seamlessly with IaC tools, supporting both with Terraform providers and Pulumi providers natively.

Project Setup

We will create a new app for this comparison called "demo" featuring a storage bucket for files and a photos API with routes that interact with the bucket.

Create the new Nitric project using the Nitric CLI and the nitric new command:

nitric new demo ts-starter
At the time of writing the Terraform providers are in preview, so we need to enable 'beta-providers' in the Nitric project

Add beta-providers to the preview field of the nitric.yaml to enable Terraform:

name: demo
services:
  - match: services/*.ts
    start: npm run dev:services $SERVICE_PATH
preview:
  - beta-providers

Remove the hello service and create the photos service:

cd demo/services
rm hello.ts
touch photos.ts

Add the application code into the photos.ts file:

import { api, bucket } from '@nitric/sdk'

const photosAPI = api('photos')

const files = bucket('files').allow('read', 'write')

photosAPI.get('/:name', async (ctx) => {
  const { name } = ctx.req.params

  const data = await files.file(name).read()

  ctx.res.body = data
  ctx.res.headers['Content-Type'] = ['image/jpeg']

  return ctx
})

photosAPI.post('/:name', async (ctx) => {
  const { name } = ctx.req.params
  const data = ctx.req.data

  await files.file(name).write(data)

  return ctx
})

Deploying with Terraform

We create a Nitric stack using the Nitric AWS Terraform provider. This setup generates a Terraform project, allowing deployment using Terraform’s powerful domain-specific language (HCL).

Key Steps:

  1. Create a new Nitric stack and set the region

    nitric stack new tf aws-tf
    

    In the nitric.tf.yaml file add the region:

    # The nitric provider to use
    provider: nitric/awstf@latest
    # The target aws region to deploy to
    # See available regions:
    # https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
    region: us-east-1
    
  2. Run nitric up to generate a Terraform stack

    This command will synthesize your app code into a Terraform stack

    nitric up -s tf
    

    You will see a new directory called cdktf.out containing all the Terraform modules and assets required to deploy your app.

  3. Initialize and Plan

    Navigate into cdktf.out and use terraform init to set up the configuration and terraform plan to review resources.

    cd cdktf.out/stacks/demo-tf/
    terraform init
    terraform plan
    
  4. Deploy

    Execute terraform apply to deploy the infrastructure.

    terraform apply
    

    The output of running the command will look something like this:

     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:
    
       # module.api_photos.aws_apigatewayv2_api.api_gateway will be created
       + resource "aws_apigatewayv2_api" "api_gateway" {
           ...
         }
    
       # module.api_photos.aws_apigatewayv2_stage.stage will be created
       + resource "aws_apigatewayv2_stage" "stage" {
           ...
         }
    
       # module.api_photos.aws_lambda_permission.apigw_lambda["demo_services-photos"] will be created
       + resource "aws_lambda_permission" "apigw_lambda" {
           ...
         }
    
       # module.bucket_files.aws_s3_bucket.bucket will be created
       + resource "aws_s3_bucket" "bucket" {
           ...
         }
    
       # module.policy_72c769e2d65d0a4787e80f72424f011d.aws_iam_role_policy.policy["demo_services-photos:Service"] will be created
       + resource "aws_iam_role_policy" "policy" {
           ...
         }
    
       # module.service_demo_services-photos.aws_ecr_repository.repo will be created
       + resource "aws_ecr_repository" "repo" {
           ...
         }
    
       # module.service_demo_services-photos.aws_iam_role.role will be created
       + resource "aws_iam_role" "role" {
           ...
         }
    
       # module.service_demo_services-photos.aws_iam_role_policy.resource-list-access will be created
       + resource "aws_iam_role_policy" "resource-list-access" {
         ...
         }
    
       # module.service_demo_services-photos.aws_iam_role_policy_attachment.basic-execution will be created
       + resource "aws_iam_role_policy_attachment" "basic-execution" {
           ...
         }
    
       # module.service_demo_services-photos.aws_lambda_function.function will be created
       + resource "aws_lambda_function" "function" {
          ...
         }
    
       # module.service_demo_services-photos.docker_registry_image.push will be created
       + resource "docker_registry_image" "push" {
          ...
         }
    
       # module.service_demo_services-photos.docker_tag.tag will be created
       + resource "docker_tag" "tag" {
          ...
         }
    
       # module.stack.aws_resourcegroups_group.group will be created
       + resource "aws_resourcegroups_group" "group" {
          ...
         }
    
     Plan: 16 to add, 0 to change, 0 to destroy.
    
     module.bucket_files.random_id.bucket_id: Creating...
     module.bucket_files.random_id.bucket_id: Creation complete after 0s [id=hhXj9oSxc8U]
     module.service_demo_services-photos.aws_ecr_repository.repo: Creating...
     module.stack.aws_resourcegroups_group.group: Creating...
     module.service_demo_services-photos.aws_iam_role.role: Creating...
     module.bucket_files.aws_s3_bucket.bucket: Creating...
     module.service_demo_services-photos.aws_ecr_repository.repo: Creation complete after 1s [id=demo_services-photos]
     module.service_demo_services-photos.docker_tag.tag: Creating...
     module.service_demo_services-photos.docker_tag.tag: Creation complete after 0s [id=sha256:88b9684bdd8f616a6e6b5bc40153bcb713a14e3172b7420b28b04fe63d208cd1.772593474159.dkr.ecr.us-east-1.amazonaws.com/demo_services-photos]
     module.service_demo_services-photos.docker_registry_image.push: Creating...
     module.service_demo_services-photos.aws_iam_role.role: Creation complete after 1s [id=demo_services-photos]
     module.service_demo_services-photos.aws_iam_role_policy_attachment.basic-execution: Creating...
     module.service_demo_services-photos.aws_iam_role_policy.resource-list-access: Creating...
     module.stack.aws_resourcegroups_group.group: Creation complete after 1s [id=nitric-resource-group-uv2cull9]
     module.service_demo_services-photos.aws_iam_role_policy_attachment.basic-execution: Creation complete after 1s [id=demo_services-photos-20240725071034549800000001]
     module.service_demo_services-photos.aws_iam_role_policy.resource-list-access: Creation complete after 1s [id=demo_services-photos:resource-list-access]
     module.bucket_files.aws_s3_bucket.bucket: Creation complete after 5s [id=files-8615e3f684b173c5]
     module.service_demo_services-photos.docker_registry_image.push: Creation complete after 58s [id=sha256:50976d812d17751f8e96989ad408cc14e3c431ef012be296ecbaaf38d9ea3c30]
     module.service_demo_services-photos.aws_lambda_function.function: Creating...
     module.service_demo_services-photos.aws_lambda_function.function: Creation complete after 12s [id=demo_services-photos-uv2cull9]
     module.api_photos.aws_apigatewayv2_api.api_gateway: Creating...
     module.api_photos.aws_apigatewayv2_api.api_gateway: Creation complete after 4s [id=j3j5pua60m]
     module.api_photos.aws_lambda_permission.apigw_lambda["demo_services-photos"]: Creating...
     module.api_photos.aws_apigatewayv2_stage.stage: Creating...
     module.api_photos.aws_lambda_permission.apigw_lambda["demo_services-photos"]: Creation complete after 1s [id=terraform-20240725071148144100000003]
     module.api_photos.aws_apigatewayv2_stage.stage: Creation complete after 1s [id=$default]
    
     Apply complete! Resources: 16 added, 0 changed, 0 destroyed.
    

    View the deployed cloud resources in the AWS Console.

  5. Cleanup

    To clean up your stack, run terraform destroy:

    terraform destroy
    

Deploying with Pulumi

Pulumi, a newer player, emphasizes a developer-first experience with support for multiple programming languages. Unlike Terraform’s HCL, Pulumi uses general-purpose languages, making it accessible to a broader audience.

Key Steps:

  1. Create a new Nitric stack and set the region

    Make sure you are back in the root demo directory, then run:

    nitric stack new pulumi aws
    

    in the nitric.pulumi.yaml file add the region:

    # The nitric provider to use
    provider: nitric/aws@latest
    # The target aws region to deploy to
    # See available regions:
    # https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
    region: us-east-1
    
  2. Deploy

    Execute nitric up against the Pulumi stack to deploy the infrastructure.

    Nitric uses Pulumi's SDK in the open-source provider, simplifying the process.

    nitric up -s pulumi
    

    The output of running nitric up with Pulumi will look something like this:

    build   Building Services
    
            demo_services-photos complete
    
      up     Deploying with nitric/aws@latest
    
              Stack::pulumi
              ├─urn:pulumi:demo-pulumi::demo::pulumi:pulumi:Stack::demo-demo-pulumi                                                                 created (2m48s)
              ├─urn:pulumi:demo-pulumi::demo::random:index/randomString:RandomString::demo-pulumi-stack-name                                        created (0s)
              ├─urn:pulumi:demo-pulumi::demo::aws:resourcegroups/group:Group::stack-resource-group                                                  created (3s)
              └─urn:pulumi:demo-pulumi::demo::pulumi:providers:docker::docker-auth-provider                                                         created (1s)
              Service::demo_services-photos
              ├─aws:iam/role:Role::demo_services-photosLambdaRole                                                                                   created (3s)
              ├─aws:ecr/repository:Repository::demo_services-photos                                                                                 created (3s)
              ├─nitriccommon:Image::demo_services-photos                                                                                            created (2m9s)
              ├─aws:iam/rolePolicy:RolePolicy::demo_services-photosListAccess                                                                       created (1s)
              ├─aws:iam/rolePolicyAttachment:RolePolicyAttachment::demo_services-photosLambdaBasicExecution                                         created (2s)
              ├─docker:index/image:Image::demo_services-photos-image                                                                                created (2m6s)
              └─aws:lambda/function:Function::demo_services-photos                                                                                  created (23s)
              Bucket::files
              └─aws:s3/bucket:Bucket::files                                                                                                         created (6s)
              Api::photos
              ├─aws:apigatewayv2/api:Api::photos                                                                                                    created (4s)
              ├─aws:apigatewayv2/stage:Stage::photosDefaultStage                                                                                    created (2s)
              └─aws:lambda/permission:Permission::photosdemo_services-photos                                                                        created (2s)
              Policy::72c769e2d65d0a4787e80f72424f011d
              └─aws:iam/rolePolicy:RolePolicy::demo_services-photos-ee092333a297dafc7194c5ab223c261b                                                created (1s)
    
    nitric/aws@latest stdout:
    ────────────────────────────────────────────────────────────────────────────────────────────────────
    Deployment server started on [::]:4000
    ────────────────────────────────────────────────────────────────────────────────────────────────────
    
    result
    
    API Endpoints:
    ──────────────
    photos: https://9pus28upk3.execute-api.us-east-1.amazonaws.com
    

    View the deployed cloud resources in the AWS Console.

  3. Cleanup

    To clean up your stack, run nitric down:

    nitric down -s pulumi
    

Comparing Terraform and Pulumi

TerraformPulumi
LanguageDomain Specific Language called HashiCorp Configuration Language (HCL)JavaScript, TypeScript, Python, Go, C#, Java, and more
EcosystemMature with extensive community support and modulesGrowing with open-source model and SDKs
State ManagementUses local or remote state with options for encryptionStores encrypted state in Pulumi Cloud, other cloud services, or manage locally
ApproachDeclarativeImperative with a focus on code-first infrastructure
Ease of UseRequires learning HCLFamiliar languages make it easier for developers
ExtensibilityStrong module system for reusabilitySupports custom code and libraries
IntegrationIntegrates well with existing DevOps toolsSmooth integration with development workflows
VisualizationRequires additional tools for visualizationBuilt-in real-time visualization during deployment
LicensingBusiness Source LicenseOpen-source under Apache License

‍When to Choose Terraform

  • Proven Track Record: Best for managing traditional infrastructure like VMs and databases due to its reliability.
  • Declarative Approach: Terraform's HCL is tailored for infrastructure, making it easy to understand for those with IaC experience.
  • Extensive Ecosystem: Offers a vast selection of providers and modules, making it suitable for multi-cloud environments and diverse SaaS integrations.
  • Vibrant Community: Benefit from an extensive range of tutorials, courses, and third-party tools, supported by a large, active community.
  • Detailed Control: Offers precise management of cloud resources, suitable for intricate deployments​.

When to Choose Pulumi

  • Language Flexibility: Leverage languages like Python, TypeScript, or Go for more expressive coding, enabling reusable components and modules with familiar constructs.
  • Application-Oriented: Well-suited for modern architectures, including containers and serverless, allowing simultaneous deployment of app code and infrastructure.
  • Seamless Integration: Utilize existing libraries and tools for a flexible development environment.
  • Strong Typing and IDE Support: Benefit from strong typing, auto-completion, and error-checking with robust IDE support.
  • Open-Source Benefits: Pulumi’s open-source model offers a viable alternative if Terraform’s licensing is a concern.

Conclusion

Choosing between Terraform and Pulumi depends on your team’s needs and preferences. Both tools offer robust capabilities for cloud infrastructure management. Consider your team’s expertise, project complexity, and integration requirements when making your choice.

For further insights and discussions, join our Discord and explore the Nitric GitHub repository for our IaC providers. Thank you for reading!

Previous Post
Remotely Producing Terraform from an API
Next Post
Working with Terraform Can Be Much Faster