Terraform vs. Nitric

Nitric is not designed to replace IaC tools like Terraform but instead introduces a method of bringing developer self-service for infrastructure directly into the developer application (Nitric's default deployment engines are built with Pulumi). Nitric can be augmented through use of tools like Pulumi or Terraform and even be fully customized using such tools see Custom Providers for more details.

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

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')
})

Terraform

const AWS = require('aws-sdk')
exports.handler = async (event) => {
const {
s3: { object },
} = event
console.log(object.key, 'was created')
}
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
}

In the Terraform example we get control over absolutely every facet of our infrastructure which is great, but for the majority of use cases, a repeatable pattern of deployment is enough and scales much better than applying fine-grained configuration every time we need to define a new deployment.

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 InfrastructureInferredExplicit
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
Last updated on Oct 15, 2024