Pulumi vs. Nitric

Nitric is a framework designed to aid developers in building full cloud applications, including declaring their infrastructure and application code in one place. Pulumi is an Infrastructure as Code tool that enables developers to define infrastructure using traditional programming languages or YAML (Cuelang). The main differences between these are:

  1. Nitric is cloud-agnostic, code that is written using Nitric constructs can be deployed to any cloud. Pulumi supports many clouds, infrastructure declarations are explicitly defined for the provider that a resource is provided by.

  2. Nitric defines not only the infrastructure but how it is interacted with at runtime, so infrastructure can be automatically inferred from application code to ensure best practice deployments and least privilege security.

  3. Nitric uses Pulumi under the hood for the Out of the Box providers. Pulumi is a great option for extending infrastructure deployed by Nitric or for deploying Nitric applications to clouds not supported by Nitric by default.

  4. Nitric provides tools for locally simulating a cloud environment (using the Nitric CLI), to allow applications to be tested locally. Pulumi programs can be unit tested but applications written to be deployed will need a separate solution for local development such as LocalStack.

Side by Side

To showcase the power of the abstraction provided by Nitric here is a showcase of a Nitric program with an equivalent Pulumi program with application code.

Nitric

handle.ts
import * as nitric from '@nitric/sdk'

const bucket = nitric.bucket('my-bucket').allow('read', 'write')

bucket.on('create', '*', async (ctx) => {
  console.log(ctx.file.key, 'was created')
})

Pulumi

src/index.js
const AWS = require('aws-sdk')

exports.handler = async (event) => {
  const {
    s3: { object },
  } = event
  console.log(object.key, 'was created')
}

Pulumi

lib/handle-stack.ts
import * as pulumi from '@pulumi/pulumi'
import * as aws from '@pulumi/aws'
import * as fs from 'fs'
import * as mime from 'mime'
import * as path from 'path'

const config = new pulumi.Config()
const region = config.require('aws:region')

const bucket = new aws.s3.Bucket('s3-bucket')

const role = new aws.iam.Role('lambdaRole', {
  assumeRolePolicy: JSON.stringify({
    Version: '2012-10-17',
    Statement: [
      {
        Action: 'sts:AssumeRole',
        Effect: 'Allow',
        Principal: {
          Service: 'lambda.amazonaws.com',
        },
      },
    ],
  }),
})

const policy = new aws.iam.RolePolicy('lambdaPolicy', {
  role: role,
  policy: JSON.stringify({
    Version: '2012-10-17',
    Statement: [
      {
        Action: [
          'logs:CreateLogGroup',
          'logs:CreateLogStream',
          'logs:PutLogEvents',
        ],
        Effect: 'Allow',
        Resource: 'arn:aws:logs:*:*:*',
      },
      {
        Action: ['s3:PutObject'],
        Effect: 'Allow',
        Resource: `${bucket.arn}/*`,
      },
    ],
  }),
})

const lambdaFunction = new aws.lambda.Function('s3UploaderLambda', {
  runtime: 'nodejs14.x',
  code: new pulumi.asset.AssetArchive({
    '.': new pulumi.asset.FileArchive('./'),
  }),
  timeout: 10,
  handler: 'index.handler',
  role: role.arn,
  environment: {
    variables: {
      BUCKET_NAME: bucket.bucket,
    },
  },
})

const allowBucket = new aws.lambda.Permission('allowBucket', {
  action: 'lambda:InvokeFunction',
  function: lambdaFunction.arn,
  principal: 's3.amazonaws.com',
  sourceArn: bucket.arn,
})

const bucketNotification = new aws.s3.BucketNotification(
  'bucketNotification',
  {
    bucket: bucket.id,
    lambdaFunctions: [
      {
        lambdaFunctionArn: func.arn,
        events: ['s3:ObjectCreated:*'],
      },
    ],
  },
  {
    dependsOn: [allowBucket],
  }
)

export const bucketName = bucket.bucket
export const lambdaFunctionName = lambdaFunction.name

The below table contains the main differences that you can see in the code examples, and also some that cannot fit in such a small app, but we still would like you to know about :)

FeatureNitricPulumi
LanguageYour choiceYAML + your choice
Lines of code769
Cloud InfrastructureInferredExplicit
ExtensibilityCustom providersCustom/dynamic providers
Local SimulationBuilt-in to CLIN/A
Cross-cloud supportSame code compiles to different cloudsNo (need to write different code for different clouds)
Provisioning engineCustom providers can be written with any IaC tech (e.g. Terraform/AWS CDK)Proprietary
TestingSame tests run on local simulator and cloud, without mocksNeed mocks for local testing