Azure Infrastructure with Pulumi

Medium 28 min read

Azure Provider Setup

Why Azure with Pulumi?

The Problem: Azure's portal and ARM templates are verbose and error-prone. JSON-based templates lack loops, conditionals, and type checking.

The Solution: Pulumi gives you full programming language power to define Azure resources with IntelliSense, type safety, and reusable abstractions.

Real Impact: Teams migrating from ARM templates to Pulumi report 60% less code and 3x faster onboarding for new engineers.

Real-World Analogy

Think of Azure with Pulumi as a city planning system:

  • Resource Groups = City districts that organize related buildings
  • Virtual Networks = Road systems connecting buildings within a district
  • VMs = Office buildings where work happens
  • Azure Functions = On-demand meeting rooms you only pay for when used
  • Blob Storage = Warehouses storing files and data

Key Azure Concepts in Pulumi

Resource Groups

Every Azure resource belongs to a resource group, which acts as a logical container for lifecycle and access management.

Azure Native vs Classic

Pulumi offers two Azure providers: azure-native (auto-generated, 100% API coverage) and classic (curated, stable).

Managed Identities

Use managed identities to authenticate between Azure services without storing credentials in code.

Location Strategy

Define a consistent region strategy using Pulumi config to deploy resources to the right Azure regions.

Installing the Azure Provider

setup.sh
# Create a new Pulumi project for Azure
pulumi new azure-typescript

# Install the Azure Native provider
npm install @pulumi/azure-native

# Configure Azure location
pulumi config set azure-native:location eastus

# Login with Azure CLI (required for auth)
az login

Resource Groups & VNets

Azure Architecture with Pulumi
Resource Group: app-rg Virtual Network (10.0.0.0/16) Public Subnet (10.0.1.0/24) Virtual Machine Standard_B2s App Service Web App Private Subnet (10.0.2.0/24) CosmosDB SQL API SQL Database Managed Blob Storage StorageV2 Azure Functions Consumption Plan Key Vault Secrets & Keys Network Security Groups (NSG) applied to all subnets

Creating Resource Groups and VNets

index.ts
import * as pulumi from "@pulumi/pulumi";
import * as resources from "@pulumi/azure-native/resources";
import * as network from "@pulumi/azure-native/network";

// Create a resource group
const resourceGroup = new resources.ResourceGroup("app-rg", {
    location: "eastus",
});

// Create a virtual network
const vnet = new network.VirtualNetwork("app-vnet", {
    resourceGroupName: resourceGroup.name,
    addressSpace: { addressPrefixes: ["10.0.0.0/16"] },
});

// Public subnet
const publicSubnet = new network.Subnet("public-subnet", {
    resourceGroupName: resourceGroup.name,
    virtualNetworkName: vnet.name,
    addressPrefix: "10.0.1.0/24",
});

// Private subnet
const privateSubnet = new network.Subnet("private-subnet", {
    resourceGroupName: resourceGroup.name,
    virtualNetworkName: vnet.name,
    addressPrefix: "10.0.2.0/24",
});

// Network Security Group
const nsg = new network.NetworkSecurityGroup("web-nsg", {
    resourceGroupName: resourceGroup.name,
    securityRules: [{
        name: "allow-http",
        priority: 100,
        direction: "Inbound",
        access: "Allow",
        protocol: "Tcp",
        sourcePortRange: "*",
        destinationPortRange: "80",
        sourceAddressPrefix: "*",
        destinationAddressPrefix: "*",
    }],
});

Compute (VMs & Functions)

Creating a Virtual Machine

vm.ts
import * as compute from "@pulumi/azure-native/compute";

// Create a public IP
const publicIp = new network.PublicIPAddress("vm-pip", {
    resourceGroupName: resourceGroup.name,
    publicIPAllocationMethod: "Dynamic",
});

// Create a NIC
const nic = new network.NetworkInterface("vm-nic", {
    resourceGroupName: resourceGroup.name,
    ipConfigurations: [{
        name: "ipconfig1",
        subnet: { id: publicSubnet.id },
        publicIPAddress: { id: publicIp.id },
    }],
});

// Create a virtual machine
const vm = new compute.VirtualMachine("web-vm", {
    resourceGroupName: resourceGroup.name,
    networkProfile: {
        networkInterfaces: [{ id: nic.id }],
    },
    hardwareProfile: { vmSize: "Standard_B2s" },
    osProfile: {
        computerName: "webserver",
        adminUsername: "azureuser",
        adminPassword: pulumi.secret("P@ssw0rd1234!"),
    },
    storageProfile: {
        imageReference: {
            publisher: "Canonical",
            offer: "0001-com-ubuntu-server-jammy",
            sku: "22_04-lts",
            version: "latest",
        },
        osDisk: {
            createOption: "FromImage",
            managedDisk: { storageAccountType: "Standard_LRS" },
        },
    },
});

Creating Azure Functions

functions.ts
import * as web from "@pulumi/azure-native/web";
import * as storage from "@pulumi/azure-native/storage";

// Storage account for function app
const storageAccount = new storage.StorageAccount("funcsa", {
    resourceGroupName: resourceGroup.name,
    sku: { name: "Standard_LRS" },
    kind: "StorageV2",
});

// App Service Plan (Consumption)
const plan = new web.AppServicePlan("func-plan", {
    resourceGroupName: resourceGroup.name,
    kind: "FunctionApp",
    sku: { name: "Y1", tier: "Dynamic" },
});

// Function App
const functionApp = new web.WebApp("api-func", {
    resourceGroupName: resourceGroup.name,
    serverFarmId: plan.id,
    kind: "FunctionApp",
    siteConfig: {
        appSettings: [
            { name: "FUNCTIONS_WORKER_RUNTIME", value: "node" },
            { name: "FUNCTIONS_EXTENSION_VERSION", value: "~4" },
            { name: "AzureWebJobsStorage", value: storageAccount.name.apply(
                n => `DefaultEndpointsProtocol=https;AccountName=${n};...`
            )},
        ],
    },
});

export const functionUrl = functionApp.defaultHostName;

Storage & Databases

Blob Storage and CosmosDB

storage-db.ts
import * as documentdb from "@pulumi/azure-native/documentdb";

// Blob storage account
const dataStorage = new storage.StorageAccount("datasa", {
    resourceGroupName: resourceGroup.name,
    sku: { name: "Standard_GRS" },
    kind: "StorageV2",
    enableHttpsTrafficOnly: true,
});

// Blob container
const container = new storage.BlobContainer("uploads", {
    resourceGroupName: resourceGroup.name,
    accountName: dataStorage.name,
    publicAccess: "None",
});

// CosmosDB account
const cosmosAccount = new documentdb.DatabaseAccount("cosmos-db", {
    resourceGroupName: resourceGroup.name,
    databaseAccountOfferType: "Standard",
    locations: [{ locationName: "eastus", failoverPriority: 0 }],
    consistencyPolicy: {
        defaultConsistencyLevel: "Session",
    },
});

// CosmosDB SQL database
const database = new documentdb.SqlResourceSqlDatabase("app-db", {
    resourceGroupName: resourceGroup.name,
    accountName: cosmosAccount.name,
    resource: { id: "app-db" },
});

export const cosmosEndpoint = cosmosAccount.documentEndpoint;

Identity & Access

RBAC and Managed Identities

identity.ts
import * as authorization from "@pulumi/azure-native/authorization";
import * as managedidentity from "@pulumi/azure-native/managedidentity";

// User-assigned managed identity
const identity = new managedidentity.UserAssignedIdentity("app-identity", {
    resourceGroupName: resourceGroup.name,
});

// Assign Storage Blob Data Contributor role
const roleAssignment = new authorization.RoleAssignment("blob-contributor", {
    principalId: identity.principalId,
    principalType: "ServicePrincipal",
    roleDefinitionId: pulumi.interpolate`/subscriptions/${subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe`,
    scope: dataStorage.id,
});

export const identityClientId = identity.clientId;

Quick Reference

Azure Resource Cheat Sheet

Resource Pulumi Module Key Properties
Resource Group resources.ResourceGroup location, tags
Virtual Network network.VirtualNetwork addressSpace, resourceGroupName
Virtual Machine compute.VirtualMachine vmSize, osProfile, storageProfile
Function App web.WebApp serverFarmId, siteConfig, kind
Storage Account storage.StorageAccount sku, kind, enableHttpsTrafficOnly
CosmosDB documentdb.DatabaseAccount locations, consistencyPolicy
Managed Identity managedidentity.UserAssignedIdentity resourceGroupName

Azure CLI Authentication

Authentication Methods

  • az login - Interactive browser login for development
  • Service Principal - Set ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID env vars
  • Managed Identity - Automatic when running on Azure VMs or App Service
  • Azure CLI - Pulumi uses cached credentials from az login