Terraform vs. Nitric

Nitric is a framework designed to aid developers in building full cloud applications, including infrastructure. Terraform is an Infrastructure as Code (IaC) tool that enables explicit definition of infrastructure using HCL (or programming languages using CDKTF). The main differences between these are:

  1. Nitric is cloud-agnostic, code that is written using Nitric constructs can be deployed to any cloud. Terraform 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 provides tools for locally simulating a cloud environment (using the Nitric CLI), to allow an application to be tested locally. Terraform can be validated and linted as well as contract tested using terraform specific tooling.

Code Comparison

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

Nitric

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

const bucket = nitric.bucket('my-bucket').for('reading', 'writing')

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

Terraform

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

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

Terraform

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
  region = "us-west-2"
}

locals {
  lambda_function_name = "upload_hello_txt_lambda"
}

resource "aws_s3_bucket" "this" {
  bucket = "my-s3-bucket"
  acl    = "private"
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "index.js"
  output_path = "${path.module}/lambda.zip"
}

resource "aws_lambda_function" "this" {
  function_name = local.lambda_function_name
  role          = aws_iam_role.lambda_role.arn
  handler       = "index.handler"
  runtime       = "nodejs14.x"
  filename      = data.archive_file.lambda_zip.output_path
  timeout       = 10

  environment {
    variables = {
      BUCKET_NAME = aws_s3_bucket.this.bucket
    }
  }
}

resource "aws_lambda_permission" "allow_bucket" {
  statement_id  = "AllowExecutionFromS3Bucket"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.func.arn
  principal     = "s3.amazonaws.com"
  source_arn    = aws_s3_bucket.bucket.arn
}

resource "aws_s3_bucket_notification" "bucket_notification" {
  bucket = aws_s3_bucket.bucket.id

  lambda_function {
    lambda_function_arn = aws_lambda_function.func.arn
    events              = ["s3:ObjectCreated:*"]
    filter_prefix       = "AWSLogs/"
    filter_suffix       = ".log"
  }

  depends_on = [aws_lambda_permission.allow_bucket]
}

resource "aws_iam_role" "lambda_role" {
  name = "lambda_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "lambda_policy" {
  name = "lambda_policy"
  role = aws_iam_role.lambda_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
      {
        Action = [
          "s3:PutObject"
        ]
        Effect   = "Allow"
        Resource = "${aws_s3_bucket.this.arn}/*"
      }
    ]
  })
}

output "bucket_name" {
  value = aws_s3_bucket.this.bucket
}

output "lambda_function_name" {
  value = aws_lambda_function.this.function_name
}

Differences

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

FeatureNitricTerraform
LanguageYour choiceHCL
Lines of code7123
Cloud InfrasutrctureInferredExplicit
ExtensibilityCustom providersCustom/dynamic providers
Local SimulationBuilt-in to CLIN/A
Cross-cloud supportSame code compiles to different cloudsNo (need to write different config 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