Provisioning Azure Virtual Machines with Pulumi
Learn how to launch an SSH-key-authenticated Linux VM on Azure with Pulumi and TypeScript, including its network interface and public IP
Provisioning Azure Virtual Machines with Pulumi
This guide launches a Linux VM on Azure with Pulumi, building on the VNet guide - a public IP, a network interface, an NSG scoped to the VM, and the VM itself with SSH key authentication (no passwords).
Prerequisites
- The resource group and subnets from the Pulumi Azure VNet guide
- An SSH key pair (
ssh-keygen -t ed25519) - the public key is passed in as config, never generated or stored by the program itself
pulumi config set sshPublicKey "$(cat ~/.ssh/id_ed25519.pub)"
Public IP and Network Interface
Unlike AWS, where a public IP can be attached directly via an instance argument, Azure VMs always go through an explicit NetworkInterface resource, and a public IP (if you want one) is its own resource attached to that NIC’s IP configuration:
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as network from "@pulumi/azure-native/network";
import * as compute from "@pulumi/azure-native/compute";
const config = new pulumi.Config();
const sshPublicKey = config.require("sshPublicKey");
const publicIp = new network.PublicIPAddress("web", {
resourceGroupName: resourceGroup.name,
location: location,
publicIpAddressName: "web-ip",
publicIPAllocationMethod: network.IPAllocationMethod.Static,
sku: { name: network.PublicIPAddressSkuName.Standard },
});
const webNsg = new network.NetworkSecurityGroup("web", {
resourceGroupName: resourceGroup.name,
location: location,
networkSecurityGroupName: "web-nsg",
securityRules: [{
name: "AllowSSH",
priority: 100,
direction: network.SecurityRuleDirection.Inbound,
access: network.SecurityRuleAccess.Allow,
protocol: network.SecurityRuleProtocol.Tcp,
sourcePortRange: "*",
destinationPortRange: "22",
sourceAddressPrefix: "*",
destinationAddressPrefix: "*",
}],
});
const nic = new network.NetworkInterface("web", {
resourceGroupName: resourceGroup.name,
location: location,
networkInterfaceName: "web-nic",
ipConfigurations: [{
name: "ipconfig1",
subnet: { id: publicSubnet.id },
publicIPAddress: { id: publicIp.id },
}],
networkSecurityGroup: { id: webNsg.id },
});
Note the inconsistent casing between publicIpAddressName (lowercase “p” in “Ip”) and publicIPAddress/PublicIPAddress elsewhere (uppercase “IP”) - both are real, current property/type names in the provider, just generated from ARM specs that aren’t perfectly self-consistent. TypeScript will catch a mismatch immediately if you get one wrong.
The Virtual Machine
const vm = new compute.VirtualMachine("web", {
resourceGroupName: resourceGroup.name,
location: location,
vmName: "web-vm",
hardwareProfile: {
vmSize: "Standard_B2s",
},
networkProfile: {
networkInterfaces: [{
id: nic.id,
primary: true,
}],
},
osProfile: {
computerName: "webvm",
adminUsername: "azureuser",
linuxConfiguration: {
disablePasswordAuthentication: true,
ssh: {
publicKeys: [{
path: "/home/azureuser/.ssh/authorized_keys",
keyData: sshPublicKey,
}],
},
},
},
storageProfile: {
imageReference: {
publisher: "Canonical",
offer: "0001-com-ubuntu-server-jammy",
sku: "22_04-lts-gen2",
version: "latest",
},
osDisk: {
createOption: compute.DiskCreateOptionTypes.FromImage,
managedDisk: {
storageAccountType: compute.StorageAccountTypes.Standard_LRS,
},
},
},
});
export const vmId = vm.id;
export const publicIpAddress = publicIp.ipAddress;
disablePasswordAuthentication: true plus an SSH public key is the direct equivalent of omitting an admin_password and setting disable_password_authentication = true in the Terraform azurerm_linux_virtual_machine resource - the VM never has a working password at all, only key-based auth.
Best Practices
- Never pass a plaintext
adminPassword- uselinuxConfiguration.ssh.publicKeys(Linux) or a Pulumi secret config value if a Windows VM genuinely needs a password. - Size with
Standard_B2s-class burstable VMs for dev/test, and move to a fixed-performance SKU (Standard_D-series) once workload profiling shows sustained CPU use - burstable VMs throttle hard once their credit balance runs out. - Scope the NSG to the VM’s actual exposed ports - here just SSH; add application ports as separate rules rather than widening
destinationPortRangeto a range.
Conclusion
Compare this to the EC2 guide in the AWS series: the concepts are identical (network interface, security group, image reference, key-based auth) but Azure’s ARM-modeled resources need an explicit NIC and a hardwareProfile/osProfile/storageProfile/networkProfile split where AWS flattens most of that onto a single Instance resource.
For more Pulumi topics, check out: