Back to Blog

Getting Started with Pulumi

Tobias Schmidt
by Tobias Schmidt
Getting Started with Pulumi

Table of Contents

Jump to a section

Introduction

Pulumi is a modern Infrastructure as Code platform. Like other tools like Terraform, you can define your whole infrastructure in code.

The main difference is that Pulumi is not limited to a specific cloud provider. You can use it to deploy your infrastructure to multiple cloud providers, including AWS, Azure, Google Cloud, and Kubernetes.

Additionally, Pulumi is not limited to just cloud resources. It's also possible to deploy your infrastructure to other platforms, like Docker, Kubernetes, and even your own data center.

Furthermore, Pulumi allows you to use your favorite programming language. This means, you can use Python, TypeScript, Go, JavaScript, and more.

In this article, we will look at how to get started with Pulumi. At end, you'll know everything you're set up to start building your own infrastructure.

Key Concepts

Like Terraform, Pulumi is also a declarative tool. This means, we'll describe our desired state of our infrastructure. Pulumi will then take care of creating this state through actions.

Key Concepts of Pulumi: State Storage, Pulumi Engine, Providers and the Language Host

On the host, we'll write our code in a programming language of our choice. Pulumi will then compute this into a plan, which is a list of actions that need to be taken to reach the desired state.

This means it will calculate which resources need to be

  • created,
  • updated,
  • or deleted.

The state is later stored in a stack. This stack can live in a remote or local directory.

For serious projects, it's a requirement to use a remote stack, as otherwise, collaboration is not possible.

Projects

For Pulumi, every folder that contains a Pulumi.yaml file is considered a project. If there's no Pulumi project yet, you can create one by running pulumi new.

As we've learned, the supported runtimes are nodejs, python, dotnet, go, java and yaml.

The project file itself is a simple, short YAML file:

name: awsfundamentals
runtime: nodejs
description: A simple Pulumi project

When using NodeJS, by default, Pulumi will use TypeScript. You can change this by setting options.typescript to false.

With NodeJS based projects, we'll also need a package.json file. This way, we can install dependencies that are needed for our project.

Also worth noticing, everything you refer to on the local filesystem needs to be relative to the project's root.

Stacks

Stacks are a way to manage different environments. For example, we can have a dev and a prod stack.

Pulumi Stacks

Each stack can have it's own configuration. When we create a new stack, Pulumi will also create a Pulumi.$STACK_NAME.yaml file.

This file can contain any configuration that we want to use for this stack.

config:
  awsfundamentals:
    propertyOne: valueOne
    propertyTwo:
      key: value

You can set the configuration properties via the CLI or directly in the YAML file.

pulumi config set -s <stack-name> <property-name> <value>

Inside your infrastructure file, e.g. your index.ts in the case of a NodeJS project, you can access your configuration properties and your stack name at any time.

import { Config, getStack } from '@pulumi/pulumi';

const config = new Config();
const propertyOne = config.require("propertyOne");
const stackName = getStack();

The config object also has other methods, like requireObject which allows us to require a configuration object. If it's not set, it will throw an error.

const config = new Config();
const propertyOne = config.require("propertyOne");
const propertyTwo = config.requireObject<{ key: string }>("propertyTwo");

Resources

Resources are the main building blocks in Pulumi. They actually make up your infrastructure.

In case of AWS, a resource is a single AWS resource, like a VPC, a security group, an EC2 instance, etc.

There are two subclasses of resources:

  1. CustomResource: a cloud resource managed by a resource provider, e.g. AWS.
  2. ComponentResource: a resource that is composed of other resources, e.g. a VPC and an EC2 instance. These are the higher building blocks that are used to create more complex infrastructure via composition.

Higher building blocks are also called components. They are very useful when we want to create reusable infrastructure.

Components in Pulumi

In the example above, we're having a component that is composed of resources needed to run a containerized application in Fargate.

Inputs and Outputs

Now to one of the most important concepts in Pulumi: inputs and outputs.

All resources that Pulumi accepts are called inputs. For example, the aws.lambda.Function resource accepts inputs like name, handler, runtime, etc.

const myLambda = new aws.lambda.Function('myLambda', {
  name: 'my-function',
  handler: 'index.handler',
  runtime: 'nodejs22.x',
});

Pulumi will later translate this into a plan and map the inputs to the API call's parameters.

Outputs are the other way around. They are the values that are returned by a resource.

For example, the aws.lambda.Function resource returns an arn or name output.

const lambdaArn = myLambda.arn;
const lambdaName = myLambda.name;

The output is calculated when the resource is created. Pulumi can't know it beforehand, as it's a runtime value.

Nevertheless, we can still access it via the arn output property.

Now, here comes the magic of Pulumi: We can also use this output as an input for other resources.

const cloudWatchLogGroup = new aws.cloudwatch.LogGroup('myLogGroup', {
  name: `/aws/lambda/${myLambda.name}`,
  retentionInDays: 14,
});

This way, we can create a log group for our lambda function. We don't need to hardcode the name of the log group, as it's dynamically created by Pulumi.

If we'd change the name of the lambda function, the log group would automatically be renamed to match the new lambda function's name.

Secrets

Secrets are a way to manage sensitive information, like passwords, API keys, etc. This way, you don't need to work with plain text secrets in your Pulumi configuration files.

With the CLI, we can easily encrypt secrets and push them to our configuration files.

pulumi config set --secret mySecret mySecretValue

This will encrypt the secret and save it to the configuration file.

By default, Pulumi will use a key that is managed by the Pulumi Cloud. Alternatively, you can also use other key management systems, like AWS KMS, Azure Key Vault, and Google Cloud KMS. This way, the keys are managed by you or your organization.

Inside our code, we can access the secret via the Config object.

const config = new Config();
const mySecret = config.requireSecret("mySecret");

The great thing here: Pulumi will never print the secret value to the console. Even if we decide to explicitly print the secret value, it will be masked.

console.log(`Password: ${mySecret}`);
$ pulumi up

Password: [secret]

State Management and Backends

When Pulumi creates resource for you, it needs to store metadata so it knows what it created. This metadata is stored in the state file.

This state file should live in a remote backend; else, collaboration will be a pain.

Luckily, Pulumi is very flexible when it comes to state management.

We can choose where to store the state file. Options include but are not limited to:

  • AWS S3
  • Microsoft Azure Blob Storage
  • Google Cloud Storage

Alternatively, you can also use the Pulumi Cloud as a backend.

When you decide to use a remote backend, you always need to login before using the CLI.

pulumi login <backend-url>

# e.g. for Amazon S3
pulumi login s3://my-bucket
# ... or with an Azure Blob Storage
pulumi login azblob://my-container

Hands-On

Now that we know the key concepts, let's bring them to life.

What we'll do:

  1. Configuring Pulumi so that it's able to deploy to AWS.
  2. Creating a new project and stack.
  3. Provisioning a simple S3 bucket via the Pulumi starter template.
  4. Deploying everything.
  5. Destroying the stack and cleaning up.

Prerequisites and Installation

Before we start, we need to install Pulumi.

  • 🍏 macOS: we can simple install Pulumi via Homebrew.
  • 🐧 Linux: we can use curl to run the installation script for the latest version via curl -fsSL https://get.pulumi.com | sh.
  • 🪟 Windows: by using PowerShell, we can use chocolatey to install Pulumi via choco install pulumi.

Choosing our Programming Language

As mentioned earlier, Pulumi doesn't come with it's own language. Instead, we can use our favorite programming language.

Supported languages are:

  • Python
  • TypeScript
  • Go
  • JavaScript
  • C#
  • Java

You can also make use of YAML, but I don't recommend it.

In this guide, we'll use TypeScript.

Setting up our AWS Access

We'll now install the AWS CLI. When using macOS, you can use homebrew via brew install awscli to get the latest version. For Linux and Windows, there's a similar process.

Configuration of our Credentials

You need to be authenticated and authorized to access your account via the AWS CLI. This requires you to have an Access Key ID and Secret Access Key.

Let's head over to your user's security credentials settings so we can create a pair of those if you haven't done this already.

Security Best Practice: While using long-term access keys is convenient for this tutorial, it's recommended to use temporary session tokens with Multi-Factor Authentication (MFA) for enhanced security in production environments. Learn more in our guide on AWS Authentication Best Practices.

We'll select the Command Line Interface (CLI). The AWS console may note that it's recommended to make use of the IAM Identity Center to access the AWS CLI.

Make sure to save your Secret Access Key, as you can't display it again.

Now we can jump back into our terminal and run aws configure which will prompt us for our previously created Access Key and Secret Access Key. It will also ask for a default region, e.g. us-east-1, and the default output format, e.g. json.

If you've set up everything properly, you should be able to get a few details of your account and user by running aws sts get-caller-identity.

{
    "UserId": "AROASJEZZYDC2DOJKSDXY:john.doe",
    "Account": "012345678901",
    "Arn": "arn:aws:sts::012345678901:assumed-role/AWSReservedSSO_AdministratorAccess_bc0e725dc894596b/john.doe"
}

Now we're ready to start building!

Creating a New Project and Stack

We'll store our state in an Amazon S3 bucket. Let's create that bucket first

aws s3 mb s3://pulumi-hands-on-awsf
make_bucket: pulumi-hands-on-awsf

Now, we can already log in to Pulumi.

pulumi login s3://pulumi-hands-on-awsf
Logged in to localhost as john.doe (s3://pulumi-hands-on-awsf)

Now, we can create a new project by using the pulumi new command and using the aws-typescript template.

mkdir pulumi-hands-on-awsf
cd pulumi-hands-on-awsf
pulumi new aws-typescript

You'll be asked a lot of questions while going through the process, including:

  • The name of the project
  • Project description
  • Stack name
  • Phrase to protect the secrets
  • Your desired package manager
  • The AWS region to deploy to

Afterward, you'll see a success message like this:

Your new project is ready to go! ✨
To perform an initial deployment, run `pulumi up`

Our project is now ready to go! 🎉

Let's have a look at our index.ts file.

import * as aws from '@pulumi/aws';

// Create an AWS resource (S3 Bucket)
const bucket = new aws.s3.BucketV2('my-bucket');

// Export the name of the bucket
export const bucketName = bucket.id;

It's a very minimalistic example, but it's enough to get us started.

Now, we can already perform an initial deployment.

pulumi up

Pulumi will now translate our TypeScript code into a plan.

Pulumi Plan

As we can see, Pulumi detected that we want to create a new S3 bucket. We can now decide whether we want to apply this plan or not (this can also be skipped by using the --yes flag).

Let's confirm the plan and apply it.

Pulumi Up

Now, we can head over to our S3 bucket and see that it's been created.

aws s3 ls | grep my-bucket
2025-04-17 08:30:44 my-bucket-8690ee6

Now, we can destroy the stack and clean up!

pulumi destroy

Pulumi Destroy

Now, we can head over to our S3 bucket and see that it's been deleted.

aws s3 ls | grep my-bucket

No results should be returned.

That's already it! 🎉

You've learned the key concepts of Pulumi and how to create a new project and stack.

Summary

In this article, we've learned the key concepts of Pulumi. We've also learned how to create a new project and stack.

We've then deployed a simple S3 bucket and cleaned up after ourselves.

Generally, Pulumi is a very powerful and flexible tool to manage infrastructure. Due to the many supported languages, it's also very easy to integrate into your existing workflow. Developers don't need to learn a new language or tool, but they can keep using their favorite language. Even for the infrastructure part.

I hope you've enjoyed this article and you're excited to start building with Pulumi.