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
- All necessary tools installed (Nitric, Docker, Pulumi and Terraform)
- AWS credentials configured locally
- You can find the source code in the main repo here.
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
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:
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
Run
nitric up
to generate a Terraform stackThis 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.Initialize and Plan
Navigate into
cdktf.out
and useterraform init
to set up the configuration andterraform plan
to review resources.cd cdktf.out/stacks/demo-tf/ terraform init terraform plan
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.
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:
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
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.
Cleanup
To clean up your stack, run
nitric down
:nitric down -s pulumi
Comparing Terraform and Pulumi
Terraform | Pulumi | |
---|---|---|
Language | Domain Specific Language called HashiCorp Configuration Language (HCL) | JavaScript, TypeScript, Python, Go, C#, Java, and more |
Ecosystem | Mature with extensive community support and modules | Growing with open-source model and SDKs |
State Management | Uses local or remote state with options for encryption | Stores encrypted state in Pulumi Cloud, other cloud services, or manage locally |
Approach | Declarative | Imperative with a focus on code-first infrastructure |
Ease of Use | Requires learning HCL | Familiar languages make it easier for developers |
Extensibility | Strong module system for reusability | Supports custom code and libraries |
Integration | Integrates well with existing DevOps tools | Smooth integration with development workflows |
Visualization | Requires additional tools for visualization | Built-in real-time visualization during deployment |
Licensing | Business Source License | Open-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!