Problem Context
Infrastructure code is just code โ it deserves the same quality standards as your application code: type safety, refactoring support, documented interfaces, and testability. But in YAML, HCL, and JSON, your editor can't tell you when you pass a number where a string is expected, or when you reference a property that doesn't exist.
Pulumi with TypeScript applies static typing to cloud infrastructure. Every Azure resource type has a generated TypeScript interface. Mismatched properties, missing required inputs, and invalid configurations become compile errors โ not runtime surprises after a failed deployment.
- A typo in an ARM template property name caused a silent misconfiguration
- You want to use TypeScript generics to model environment-specific infrastructure variations
- Your team is already TypeScript-fluent and wants one less language to maintain
- You need to test infrastructure logic with Jest before spending deploy time
Pulumi TypeScript: your infrastructure gets the same compiler guarantees as your application code.
Project Setup
# Create a new TypeScript Azure project
pulumi new azure-typescript --name myapp --stack dev
# Project structure
myapp/
โโโ index.ts # Infrastructure entry point
โโโ Pulumi.yaml # Project metadata
โโโ Pulumi.dev.yaml # Stack config
โโโ package.json
โโโ tsconfig.json
โโโ node_modules/// package.json (key sections)
{
"devDependencies": {
"@pulumi/pulumi": "^3.0.0",
"@pulumi/azure-native": "^2.0.0",
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}Type-Safe Resource Declarations
// index.ts
import * as pulumi from '@pulumi/pulumi';
import * as azure from '@pulumi/azure-native';
const config = new pulumi.Config();
const environment = config.require('environment');
const location = config.get('location') ?? 'westus2';
// TypeScript knows every valid property and its type
const rg = new azure.resources.ResourceGroup(`rg-${environment}`, {
resourceGroupName: `rg-myapp-${environment}`,
location,
tags: {
Environment: environment,
ManagedBy: 'pulumi',
},
});
const storage = new azure.storage.StorageAccount(`sa-${environment}`, {
accountName: `samyapp${environment}001`,
resourceGroupName: rg.name,
location: rg.location,
sku: { name: azure.storage.SkuName.Standard_LRS },
kind: azure.storage.Kind.StorageV2,
allowBlobPublicAccess: false, // TypeScript: boolean, not string
minimumTlsVersion: azure.storage.MinimumTlsVersion.TLS1_2,
tags: rg.tags,
});
export const storageAccountName = storage.name;
export const blobEndpoint = storage.primaryEndpoints.apply(ep => ep.blob);Working with Output<T>
Resources return Output<T>for values resolved after deployment. TypeScript's type system tracks this for you:
import * as pulumi from '@pulumi/pulumi';
// Output<string> โ can't use directly as a string
const name: pulumi.Output<string> = storage.name;
// Transform with .apply()
const connectionString: pulumi.Output<string> = storage.name.apply(
n => `DefaultEndpointsProtocol=https;AccountName=${n};...`
);
// Combine multiple outputs
const message: pulumi.Output<string> = pulumi.interpolate`
Storage account ${storage.name} deployed in ${rg.location}
`;
// all() โ wait for multiple outputs
const summary = pulumi.all([storage.name, rg.location]).apply(
([name, loc]) => `${name} @ ${loc}`
);ComponentResource: Typed Infrastructure Modules
import * as pulumi from '@pulumi/pulumi';
import * as azure from '@pulumi/azure-native';
interface AppStorageArgs {
resourceGroupName: pulumi.Input<string>;
location: pulumi.Input<string>;
environment: string;
replicationEnabled?: boolean;
}
class AppStorage extends pulumi.ComponentResource {
public readonly accountName: pulumi.Output<string>;
public readonly blobEndpoint: pulumi.Output<string | undefined>;
constructor(
name: string,
args: AppStorageArgs,
opts?: pulumi.ComponentResourceOptions,
) {
super('myapp:storage:AppStorage', name, {}, opts);
const sku = args.replicationEnabled
? azure.storage.SkuName.Standard_GRS
: azure.storage.SkuName.Standard_LRS;
const account = new azure.storage.StorageAccount(
`${name}-sa`,
{
accountName: `sa${name}${args.environment}001`.toLowerCase(),
resourceGroupName: args.resourceGroupName,
location: args.location,
sku: { name: sku },
kind: azure.storage.Kind.StorageV2,
allowBlobPublicAccess: false,
minimumTlsVersion: azure.storage.MinimumTlsVersion.TLS1_2,
},
{ parent: this },
);
this.accountName = account.name;
this.blobEndpoint = account.primaryEndpoints.apply(ep => ep.blob);
this.registerOutputs({
accountName: this.accountName,
blobEndpoint: this.blobEndpoint,
});
}
}
// Usage
const storage = new AppStorage('app', {
resourceGroupName: rg.name,
location: rg.location,
environment: 'prod',
replicationEnabled: true,
});
export const blobEndpoint = storage.blobEndpoint;Testing with Jest
// __tests__/storage.test.ts
import * as pulumi from '@pulumi/pulumi';
// Mock provider
pulumi.runtime.setMocks({
newResource: (args) => ({
id: args.inputs.accountName + '_id',
state: args.inputs,
}),
call: (args) => ({ result: args.inputs }),
});
// Import after setting mocks
import '../index';
describe('Storage Account Security', () => {
test('public access is disabled', done => {
const infra = require('../index');
infra.storageAccount.allowBlobPublicAccess.apply(
(allowPublic: boolean) => {
expect(allowPublic).toBe(false);
done();
}
);
});
test('TLS version is 1.2', done => {
const infra = require('../index');
infra.storageAccount.minimumTlsVersion.apply(
(tls: string) => {
expect(tls).toBe('TLS1_2');
done();
}
);
});
});Stack References: Cross-Stack Outputs
// Consume outputs from another stack
const networkStack = new pulumi.StackReference(
'org/network/prod' // or 'org/network/${environment}'
);
const vnetId = networkStack.getOutput('vnetId') as pulumi.Output<string>;
const subnetId = networkStack.getOutput('appSubnetId') as pulumi.Output<string>;
// Use in this stack's resources
const appService = new azure.web.WebApp('app', {
virtualNetworkSubnetId: subnetId,
// ...
});Production Checklist
- Enable
strict: trueintsconfig.jsonโ catch null/undefined issues at compile time - Type all
ComponentResourceargs interfaces โ avoidany - Write Jest tests for security-critical properties (public access, TLS, encryption)
- Use
pulumi.interpolatefor string templates that includeOutput<T> - Pin
@pulumi/azure-nativeto a specific minor version; review provider changelogs before upgrading - Use
ResourceOptions.protect: trueon databases and state stores - Run
pulumi previewin CI on every pull request; require approval for production deploys

