Provisioning an Azure Virtual Network with Pulumi
Learn how to build an Azure VNet with public and private subnets and a Network Security Group using Pulumi and TypeScript
Provisioning an Azure Virtual Network with Pulumi
This guide builds the same VNet topology as the Terraform VNet guide on this site, using Pulumi and the @pulumi/azure-native provider - a resource group, a VNet with public and private subnets, and a Network Security Group the rest of this Azure Pulumi series builds on.
azure-native vs. the Classic azure Provider
Pulumi has two Azure providers: @pulumi/azure (“Classic”, bridged from the Terraform azurerm provider, the same relationship @pulumi/aws has to Terraform’s AWS provider) and @pulumi/azure-native (generated directly from Azure Resource Manager’s API specs). This series uses azure-native - it’s Pulumi’s current recommended default for new Azure projects, covers the full ARM API surface immediately as Azure ships it rather than waiting on a bridge update, and its resource/argument names map directly onto ARM’s own naming rather than a Terraform-specific schema.
Prerequisites
npm install @pulumi/pulumi @pulumi/azure-native
- Pulumi CLI and
az logincompleted (the provider uses your Azure CLI credentials by default, same as Terraform’sazurermprovider)
Resource Group
Every Azure resource lives inside a resource group - there’s no equivalent “no group” option the way AWS or GCP resources can exist without an enclosing container:
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as resources from "@pulumi/azure-native/resources";
import * as network from "@pulumi/azure-native/network";
const config = new pulumi.Config();
const location = config.get("location") ?? "eastus";
const resourceGroup = new resources.ResourceGroup("main", {
resourceGroupName: "app-rg",
location: location,
});
Virtual Network and Subnets
Unlike AWS’s aws.ec2.Subnet, azure-native’s Subnet is a separate resource from the VNet that references it by name, mirroring how ARM itself models subnets as VNet sub-resources:
const vnet = new network.VirtualNetwork("main", {
resourceGroupName: resourceGroup.name,
location: resourceGroup.location,
virtualNetworkName: "app-vnet",
addressSpace: {
addressPrefixes: ["10.0.0.0/16"],
},
});
const publicSubnet = new network.Subnet("public", {
resourceGroupName: resourceGroup.name,
virtualNetworkName: vnet.name,
subnetName: "public",
addressPrefix: "10.0.1.0/24",
});
const privateSubnet = new network.Subnet("private", {
resourceGroupName: resourceGroup.name,
virtualNetworkName: vnet.name,
subnetName: "private",
addressPrefix: "10.0.2.0/24",
});
Network Security Group
const appNsg = new network.NetworkSecurityGroup("app", {
resourceGroupName: resourceGroup.name,
location: resourceGroup.location,
networkSecurityGroupName: "app-nsg",
securityRules: [{
name: "AllowHTTPSInbound",
priority: 100,
direction: network.SecurityRuleDirection.Inbound,
access: network.SecurityRuleAccess.Allow,
protocol: network.SecurityRuleProtocol.Tcp,
sourcePortRange: "*",
destinationPortRange: "443",
sourceAddressPrefix: "*",
destinationAddressPrefix: "*",
}],
});
azure-native models ARM enum fields (direction, access, protocol here) as generated const objects like network.SecurityRuleDirection.Inbound rather than plain strings - they compile down to the same string ARM expects, but you get autocomplete and a compile error instead of a runtime rejection if you typo one.
Stack Outputs
export const resourceGroupName = resourceGroup.name;
export const vnetId = vnet.id;
export const publicSubnetId = publicSubnet.id;
export const privateSubnetId = privateSubnet.id;
export const appNsgId = appNsg.id;
Best Practices
- Attach the NSG to subnets, not individual NICs, when the same rules should apply to everything in a subnet - it’s one resource to audit instead of one per VM.
- Name resources explicitly (
resourceGroupName,virtualNetworkName, and so on) rather than relying on Pulumi’s auto-naming - ARM resource names show up directly in the Azure portal and in other teams’ Terraform/Bicep referencing your resources by name. - Keep the resource group as the blast-radius boundary -
pulumi destroyon a stack that owns a whole resource group is a much larger, more deliberate action than destroying individual resources; scope stacks accordingly.
Conclusion
This VNet is the foundation the rest of this Azure Pulumi series builds on - the VM, Storage, and AKS guides all reference resourceGroup, publicSubnet, and privateSubnet from here.
For more Pulumi topics, check out: