Virtual Machine on Azure
The Azure Virtual Machine template scaffolds a Pulumi project that provisions an Azure virtual machine inside a new Azure Virtual Network. The instance boots an HTTP server on port 80 from a small init script, so the project deploys end to end out of the box. Use it as a starting point for a web application, backend service, or database VM.
Before you begin#
For this walkthrough, you'll need:
- The Pulumi CLI
- The Azure CLI (optional, but recommended and used in this guide)
- Permission to manage virtual machines and virtual networks
For this walkthrough, you'll need:
- The Pulumi CLI
- The Azure CLI (optional, but recommended and used in this guide)
- Permission to manage virtual machines and virtual networks
For this walkthrough, you'll need:
- The Pulumi CLI
- The Azure CLI (optional, but recommended and used in this guide)
- Permission to manage virtual machines and virtual networks
- A current version of Go installed locally
For this walkthrough, you'll need:
- The Pulumi CLI
- The Azure CLI (optional, but recommended and used in this guide)
- Permission to manage virtual machines and virtual networks
- A current version of .NET installed locally
For this walkthrough, you'll need:
- The Pulumi CLI
- The Azure CLI (optional, but recommended and used in this guide)
- Permission to manage virtual machines and virtual networks
For this walkthrough, you'll need:
- A current version of the Pulumi CLI (Pulumi HCL requires no additional runtime)
- The Azure CLI (optional, but recommended and used in this guide)
- Permission to manage virtual machines and virtual networks
Create a new project#
Create a new project from the template:
mkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-typescriptmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-typescriptmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-pythonmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-pythonmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-gomkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-gomkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-csharpmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-csharpmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-yamlmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-yamlmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-hclmkdir my-virtual-machine; cd my-virtual-machinepulumi new vm-azure-hclThe pulumi new command walks you through creating and configuring the new project and stack. A stack is an instance of a project, and you can have many of them (dev, staging, prod), each with its own configuration settings. Depending on the language, you may also be asked for your preferred package manager or toolchain.
This template’s settings include:
| Key | Description | Default |
|---|---|---|
| azure-native:location | The Azure location to deploy into | WestUS2 |
| adminUsername | The user account to create on the VM | pulumiuser |
| vmName | The DNS hostname prefix to use for the VM | my-server |
| vmSize | The machine size to use for the VM | Standard_A1_v2 |
| osImage | The Azure URN of the base image to use for the VM | Debian:debian-12:12:latest |
| servicePort | The HTTP service port to expose on the VM | 80 |
pulumi config set
:
pulumi config set azure-native:location <value>pulumi config set adminUsername <value>pulumi config set vmName <value>pulumi config set vmSize <value>pulumi config set osImage <value>pulumi config set servicePort <value>pulumi config set azure-native:location "<value>"pulumi config set adminUsername "<value>"pulumi config set vmName "<value>"pulumi config set vmSize "<value>"pulumi config set osImage "<value>"pulumi config set servicePort "<value>"| Key | Description | Default |
|---|---|---|
| azure-native:location | The Azure location to deploy into | WestUS2 |
| adminUsername | The user account to create on the VM | pulumiuser |
| vmName | The DNS hostname prefix to use for the VM | my-server |
| vmSize | The machine size to use for the VM | Standard_A1_v2 |
| osImage | The Azure URN of the base image to use for the VM | Debian:debian-12:12:latest |
| servicePort | The HTTP service port to expose on the VM | 80 |
pulumi config set
:
pulumi config set azure-native:location <value>pulumi config set adminUsername <value>pulumi config set vmName <value>pulumi config set vmSize <value>pulumi config set osImage <value>pulumi config set servicePort <value>pulumi config set azure-native:location "<value>"pulumi config set adminUsername "<value>"pulumi config set vmName "<value>"pulumi config set vmSize "<value>"pulumi config set osImage "<value>"pulumi config set servicePort "<value>"| Key | Description | Default |
|---|---|---|
| azure-native:location | The Azure location to deploy into | WestUS2 |
| adminUsername | The user account to create on the VM | pulumiuser |
| vmName | The DNS hostname prefix to use for the VM | my-server |
| vmSize | The machine size to use for the VM | Standard_A1_v2 |
| osImage | The Azure URN of the base image to use for the VM | Debian:debian-12:12:latest |
| servicePort | The HTTP service port to expose on the VM | 80 |
pulumi config set
:
pulumi config set azure-native:location <value>pulumi config set adminUsername <value>pulumi config set vmName <value>pulumi config set vmSize <value>pulumi config set osImage <value>pulumi config set servicePort <value>pulumi config set azure-native:location "<value>"pulumi config set adminUsername "<value>"pulumi config set vmName "<value>"pulumi config set vmSize "<value>"pulumi config set osImage "<value>"pulumi config set servicePort "<value>"| Key | Description | Default |
|---|---|---|
| azure-native:location | The Azure location to deploy into | WestUS2 |
| adminUsername | The user account to create on the VM | pulumiuser |
| vmName | The DNS hostname prefix to use for the VM | my-server |
| vmSize | The machine size to use for the VM | Standard_A1_v2 |
| osImage | The Azure URN of the base image to use for the VM | Debian:debian-12:12:latest |
| servicePort | The HTTP service port to expose on the VM | 80 |
pulumi config set
:
pulumi config set azure-native:location <value>pulumi config set adminUsername <value>pulumi config set vmName <value>pulumi config set vmSize <value>pulumi config set osImage <value>pulumi config set servicePort <value>pulumi config set azure-native:location "<value>"pulumi config set adminUsername "<value>"pulumi config set vmName "<value>"pulumi config set vmSize "<value>"pulumi config set osImage "<value>"pulumi config set servicePort "<value>"| Key | Description | Default |
|---|---|---|
| azure-native:location | The Azure location to deploy into | WestUS2 |
| adminUsername | The user account to create on the VM | pulumiuser |
| vmName | The DNS hostname prefix to use for the VM | my-server |
| vmSize | The machine size to use for the VM | Standard_A1_v2 |
| osImage | The Azure URN of the base image to use for the VM | Debian:debian-12:12:latest |
| servicePort | The HTTP service port to expose on the VM | 80 |
pulumi config set
:
pulumi config set azure-native:location <value>pulumi config set adminUsername <value>pulumi config set vmName <value>pulumi config set vmSize <value>pulumi config set osImage <value>pulumi config set servicePort <value>pulumi config set azure-native:location "<value>"pulumi config set adminUsername "<value>"pulumi config set vmName "<value>"pulumi config set vmSize "<value>"pulumi config set osImage "<value>"pulumi config set servicePort "<value>"| Key | Description | Default |
|---|---|---|
| azure-native:location | The Azure location to deploy into | WestUS2 |
| admin_username | The user account to create on the VM | pulumiuser |
| vm_name | The computer name and DNS hostname prefix to use for the VM | my-server |
| vm_size | The machine size to use for the VM | Standard_A1_v2 |
| os_image | The Azure image reference (publisher:offer:sku:version) to use for the VM | Debian:debian-12:12:latest |
| service_port | The HTTP service port to expose on the VM | 80 |
pulumi config set
:
pulumi config set azure-native:location <value>pulumi config set admin_username <value>pulumi config set vm_name <value>pulumi config set vm_size <value>pulumi config set os_image <value>pulumi config set service_port <value>pulumi config set azure-native:location "<value>"pulumi config set admin_username "<value>"pulumi config set vm_name "<value>"pulumi config set vm_size "<value>"pulumi config set os_image "<value>"pulumi config set service_port "<value>"Review the code#
When setup completes, you’ll see a few files:
Pulumi.yamlcontains the project name and the language it’s written in.Pulumi.dev.yamlcontains thedevstack’s settings, including the values you just entered.index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tfcontains the program code that declares your cloud resources.
When setup completes, you’ll see a few files:
Pulumi.yamlcontains the project name and the language it’s written in.Pulumi.dev.yamlcontains thedevstack’s settings, including the values you just entered.index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tfcontains the program code that declares your cloud resources.
When setup completes, you’ll see a few files:
Pulumi.yamlcontains the project name and the language it’s written in.Pulumi.dev.yamlcontains thedevstack’s settings, including the values you just entered.index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tfcontains the program code that declares your cloud resources.
When setup completes, you’ll see a few files:
Pulumi.yamlcontains the project name and the language it’s written in.Pulumi.dev.yamlcontains thedevstack’s settings, including the values you just entered.index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tfcontains the program code that declares your cloud resources.
When setup completes, you’ll see a few files:
Pulumi.yamlcontains the project name and the resources it declares.Pulumi.dev.yamlcontains thedevstack’s settings, including the values you just entered.
When setup completes, you’ll see a few files:
Pulumi.yamlcontains the project name and the language it’s written in.Pulumi.dev.yamlcontains thedevstack’s settings, including the values you just entered.index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tfcontains the program code that declares your cloud resources.
Take a moment to open them up and have a look. Here’s what’s in index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tf:
import * as pulumi from "@pulumi/pulumi";import * as resources from "@pulumi/azure-native/resources";import * as network from "@pulumi/azure-native/network";import * as compute from "@pulumi/azure-native/compute";import * as random from "@pulumi/random";import * as tls from "@pulumi/tls";
// Import the program's configuration settingsconst config = new pulumi.Config();const vmName = config.get("vmName") || "my-server";const vmSize = config.get("vmSize") || "Standard_A1_v2";const osImage = config.get("osImage") || "Debian:debian-12:12:latest";const adminUsername = config.get("adminUsername") || "pulumiuser";const servicePort = config.get("servicePort") || "80";
const [ osImagePublisher, osImageOffer, osImageSku, osImageVersion ] = osImage.split(":");
// Create an SSH keyconst sshKey = new tls.PrivateKey("ssh-key", { algorithm: "RSA", rsaBits: 4096,});
// Create a resource groupconst resourceGroup = new resources.ResourceGroup("resource-group");
// Create a virtual networkconst virtualNetwork = new network.VirtualNetwork("network", { resourceGroupName: resourceGroup.name, addressSpace: { addressPrefixes: ["10.0.0.0/16"], }, subnets: [ { name: `${vmName}-subnet`, addressPrefix: "10.0.1.0/24", }, ],});
// Use a random string to give the VM a unique DNS namevar domainNameLabel = new random.RandomString("domain-label", { length: 8, upper: false, special: false,}).result.apply(result => `${vmName}-${result}`);
// Create a public IP address for the VMconst publicIp = new network.PublicIPAddress("public-ip", { resourceGroupName: resourceGroup.name, publicIPAllocationMethod: network.IPAllocationMethod.Dynamic, dnsSettings: { domainNameLabel: domainNameLabel, },});
// Create a security group allowing inbound access over ports 80 (for HTTP) and 22 (for SSH)const securityGroup = new network.NetworkSecurityGroup("security-group", { resourceGroupName: resourceGroup.name, securityRules: [ { name: `${vmName}-securityrule`, priority: 1000, direction: network.AccessRuleDirection.Inbound, access: "Allow", protocol: "Tcp", sourcePortRange: "*", sourceAddressPrefix: "*", destinationAddressPrefix: "*", destinationPortRanges: [ servicePort, "22", ], } ]});
// Create a network interface with the virtual network, IP address, and security groupconst networkInterface = new network.NetworkInterface("network-interface", { resourceGroupName: resourceGroup.name, networkSecurityGroup: { id: securityGroup.id, }, ipConfigurations: [{ name: `${vmName}-ipconfiguration`, privateIPAllocationMethod: network.IPAllocationMethod.Dynamic, subnet: { id: virtualNetwork.subnets.apply(subnets => subnets![0].id!), }, publicIPAddress: { id: publicIp.id, }, }],});
// Define a script to be run when the VM starts upconst initScript = `#!/bin/bash echo '<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> <h1>Hello, world! 👋</h1> <p>Deployed with 💜 by <a href="https://pulumi.com/">Pulumi</a>.</p> </body> </html>' > index.html sudo python3 -m http.server ${servicePort} &`;
// Create the virtual machineconst vm = new compute.VirtualMachine("vm", { resourceGroupName: resourceGroup.name, networkProfile: { networkInterfaces: [ { id: networkInterface.id, primary: true, }, ], }, hardwareProfile: { vmSize: vmSize, }, osProfile: { computerName: vmName, adminUsername: adminUsername, customData: Buffer.from(initScript).toString("base64"), linuxConfiguration: { disablePasswordAuthentication: true, ssh: { publicKeys: [ { keyData: sshKey.publicKeyOpenssh, path: `/home/${adminUsername}/.ssh/authorized_keys`, }, ], }, }, }, storageProfile: { osDisk: { name: `${vmName}-osdisk`, createOption: compute.DiskCreateOption.FromImage, }, imageReference: { publisher: osImagePublisher, offer: osImageOffer, sku: osImageSku, version: osImageVersion, }, },});
// Once the machine is created, fetch its IP address and DNS hostnameconst vmAddress = vm.id.apply(_ => network.getPublicIPAddressOutput({ resourceGroupName: resourceGroup.name, publicIpAddressName: publicIp.name,}));
// Export the VM's hostname, public IP address, HTTP URL, and SSH private keyexport const ip = vmAddress.ipAddress;export const hostname = vmAddress.dnsSettings?.apply(settings => settings?.fqdn);export const url = hostname?.apply(name => `http://${name}:${servicePort}`);export const privatekey = sshKey.privateKeyOpenssh;import pulumifrom pulumi_azure_native import resources, network, computefrom pulumi_random import random_stringimport pulumi_tls as tlsimport base64
# Import the program's configuration settingsconfig = pulumi.Config()vm_name = config.get("vmName", "my-server")vm_size = config.get("vmSize", "Standard_A1_v2")os_image = config.get("osImage", "Debian:debian-12:12:latest")admin_username = config.get("adminUsername", "pulumiuser")service_port = config.get("servicePort", "80")
os_image_publisher, os_image_offer, os_image_sku, os_image_version = os_image.split(":")
# Create an SSH keyssh_key = tls.PrivateKey( "ssh-key", algorithm="RSA", rsa_bits=4096,)
# Create a resource groupresource_group = resources.ResourceGroup("resource-group")
# Create a virtual networkvirtual_network = network.VirtualNetwork( "network", resource_group_name=resource_group.name, address_space={ "address_prefixes": [ "10.0.0.0/16", ], }, subnets=[ { "name": f"{vm_name}-subnet", "address_prefix": "10.0.1.0/24", }, ],)# Use a random string to give the VM a unique DNS namedomain_name_label = random_string.RandomString( "domain-label", length=8, upper=False, special=False,).result.apply(lambda result: f"{vm_name}-{result}")
# Create a public IP address for the VMpublic_ip = network.PublicIPAddress( "public-ip", resource_group_name=resource_group.name, public_ip_allocation_method=network.IPAllocationMethod.DYNAMIC, dns_settings={ "domain_name_label": domain_name_label, },)
# Create a security group allowing inbound access over ports 80 (for HTTP) and 22 (for SSH)security_group = network.NetworkSecurityGroup( "security-group", resource_group_name=resource_group.name, security_rules=[ { "name": f"{vm_name}-securityrule", "priority": 1000, "direction": network.AccessRuleDirection.INBOUND, "access": "Allow", "protocol": "Tcp", "source_port_range": "*", "source_address_prefix": "*", "destination_address_prefix": "*", "destination_port_ranges": [ service_port, "22", ], }, ],)
# Create a network interface with the virtual network, IP address, and security groupnetwork_interface = network.NetworkInterface( "network-interface", resource_group_name=resource_group.name, network_security_group={ "id": security_group.id, }, ip_configurations=[ { "name": f"{vm_name}-ipconfiguration", "private_ip_allocation_method": network.IPAllocationMethod.DYNAMIC, "subnet": { "id": virtual_network.subnets.apply(lambda subnets: subnets[0].id), }, "public_ip_address": { "id": public_ip.id, }, }, ],)
# Define a script to be run when the VM starts upinit_script = f"""#!/bin/bash echo '<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> <h1>Hello, world! 👋</h1> <p>Deployed with 💜 by <a href="https://pulumi.com/">Pulumi</a>.</p> </body> </html>' > index.html sudo python3 -m http.server {service_port} & """
# Create the virtual machinevm = compute.VirtualMachine( "vm", resource_group_name=resource_group.name, network_profile={ "network_interfaces": [ { "id": network_interface.id, "primary": True, } ] }, hardware_profile={ "vm_size": vm_size, }, os_profile={ "computer_name": vm_name, "admin_username": admin_username, "custom_data": base64.b64encode(bytes(init_script, "utf-8")).decode("utf-8"), "linux_configuration": { "disable_password_authentication": True, "ssh": { "public_keys": [ { "key_data": ssh_key.public_key_openssh, "path": f"/home/{admin_username}/.ssh/authorized_keys", }, ], }, }, }, storage_profile={ "os_disk": { "name": f"{vm_name}-osdisk", "create_option": compute.DiskCreateOption.FROM_IMAGE, }, "image_reference": { "publisher": os_image_publisher, "offer": os_image_offer, "sku": os_image_sku, "version": os_image_version, }, },)
# Once the machine is created, fetch its IP address and DNS hostnamevm_address = vm.id.apply( lambda id: network.get_public_ip_address_output( resource_group_name=resource_group.name, public_ip_address_name=public_ip.name, ))
# Export the VM's hostname, public IP address, HTTP URL, and SSH private keypulumi.export("ip", vm_address.ip_address)pulumi.export("hostname", vm_address.dns_settings.apply(lambda settings: settings.fqdn))pulumi.export( "url", vm_address.dns_settings.apply( lambda settings: f"http://{settings.fqdn}:{service_port}" ),)pulumi.export( "privatekey", ssh_key.private_key_openssh,)package main
import ( "encoding/base64" "fmt" "strings"
"github.com/pulumi/pulumi-azure-native-sdk/compute/v3" "github.com/pulumi/pulumi-azure-native-sdk/network/v3" "github.com/pulumi/pulumi-azure-native-sdk/resources/v3" "github.com/pulumi/pulumi-random/sdk/v4/go/random" tls "github.com/pulumi/pulumi-tls/sdk/v4/go/tls" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config")
func main() { pulumi.Run(func(ctx *pulumi.Context) error {
// Import the program's configuration settings cfg := config.New(ctx, "") vmName, err := cfg.Try("vmName") if err != nil { vmName = "my-server" } vmSize, err := cfg.Try("vmSize") if err != nil { vmSize = "Standard_A1_v2" } osImage, err := cfg.Try("osImage") if err != nil { osImage = "Debian:debian-12:12:latest" } adminUsername, err := cfg.Try("adminUsername") if err != nil { adminUsername = "pulumiuser" } servicePort, err := cfg.Try("servicePort") if err != nil { servicePort = "80" }
osImageArgs := strings.Split(osImage, ":") osImagePublisher := osImageArgs[0] osImageOffer := osImageArgs[1] osImageSku := osImageArgs[2] osImageVersion := osImageArgs[3]
// Create an SSH key sshKey, err := tls.NewPrivateKey(ctx, "ssh-key", &tls.PrivateKeyArgs{ Algorithm: pulumi.String("RSA"), RsaBits: pulumi.Int(4096), }) if err != nil { return err }
// Create a resource group resourceGroup, err := resources.NewResourceGroup(ctx, "resource-group", nil) if err != nil { return err }
// Create a virtual network virtualNetwork, err := network.NewVirtualNetwork(ctx, "network", &network.VirtualNetworkArgs{ ResourceGroupName: resourceGroup.Name, AddressSpace: network.AddressSpaceArgs{ AddressPrefixes: pulumi.ToStringArray([]string{ "10.0.0.0/16", }), }, Subnets: network.SubnetTypeArray{ network.SubnetTypeArgs{ Name: pulumi.Sprintf("%s-subnet", vmName), AddressPrefix: pulumi.String("10.0.1.0/24"), }, }, }) if err != nil { return err }
// Use a random string to give the VM a unique DNS name domainLabelSuffix, err := random.NewRandomString(ctx, "domain-label", &random.RandomStringArgs{ Length: pulumi.Int(8), Upper: pulumi.Bool(false), Special: pulumi.Bool(false), }) if err != nil { return err } domainLabel := domainLabelSuffix.Result.ApplyT(func(result string) string { return fmt.Sprintf("%s-%s", vmName, result) }).(pulumi.StringOutput)
// Create a public IP address for the VM publicIp, err := network.NewPublicIPAddress(ctx, "public-ip", &network.PublicIPAddressArgs{ ResourceGroupName: resourceGroup.Name, PublicIPAllocationMethod: pulumi.StringPtr("Dynamic"), DnsSettings: network.PublicIPAddressDnsSettingsArgs{ DomainNameLabel: domainLabel, }, }) if err != nil { return err }
// Create a security group allowing inbound access over ports 80 (for HTTP) and 22 (for SSH) securityGroup, err := network.NewNetworkSecurityGroup(ctx, "security-group", &network.NetworkSecurityGroupArgs{ ResourceGroupName: resourceGroup.Name, SecurityRules: network.SecurityRuleTypeArray{ network.SecurityRuleTypeArgs{ Name: pulumi.StringPtr(fmt.Sprintf("%s-securityrule", vmName)), Priority: pulumi.Int(1000), Direction: pulumi.String("Inbound"), Access: pulumi.String("Allow"), Protocol: pulumi.String("Tcp"), SourcePortRange: pulumi.StringPtr("*"), SourceAddressPrefix: pulumi.StringPtr("*"), DestinationAddressPrefix: pulumi.StringPtr("*"), DestinationPortRanges: pulumi.ToStringArray([]string{ servicePort, "22", }), }, }, }) if err != nil { return err }
// Create a network interface with the virtual network, IP address, and security group networkInterface, err := network.NewNetworkInterface(ctx, "network-interface", &network.NetworkInterfaceArgs{ ResourceGroupName: resourceGroup.Name, NetworkSecurityGroup: &network.NetworkSecurityGroupTypeArgs{ Id: securityGroup.ID(), }, IpConfigurations: network.NetworkInterfaceIPConfigurationArray{ &network.NetworkInterfaceIPConfigurationArgs{ Name: pulumi.String(fmt.Sprintf("%s-ipconfiguration", vmName)), PrivateIPAllocationMethod: pulumi.String("Dynamic"), Subnet: &network.SubnetTypeArgs{ Id: virtualNetwork.Subnets.ApplyT(func(subnets []network.SubnetResponse) (string, error) { return *subnets[0].Id, nil }).(pulumi.StringOutput), }, PublicIPAddress: &network.PublicIPAddressTypeArgs{ Id: publicIp.ID(), }, }, }, }) if err != nil { return err }
// Define a script to be run when the VM starts up initScript := fmt.Sprintf(`#!/bin/bash echo '<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> <h1>Hello, world! 👋</h1> <p>Deployed with 💜 by <a href="https://pulumi.com/">Pulumi</a>.</p> </body> </html>' > index.html sudo python3 -m http.server %s &`, servicePort)
// Create the virtual machine vm, err := compute.NewVirtualMachine(ctx, "vm", &compute.VirtualMachineArgs{ ResourceGroupName: resourceGroup.Name, NetworkProfile: &compute.NetworkProfileArgs{ NetworkInterfaces: compute.NetworkInterfaceReferenceArray{ &compute.NetworkInterfaceReferenceArgs{ Id: networkInterface.ID(), Primary: pulumi.Bool(true), }, }, }, HardwareProfile: &compute.HardwareProfileArgs{ VmSize: pulumi.String(vmSize), }, OsProfile: &compute.OSProfileArgs{ ComputerName: pulumi.String(vmName), AdminUsername: pulumi.String(adminUsername), CustomData: pulumi.String(base64.StdEncoding.EncodeToString([]byte(initScript))), LinuxConfiguration: &compute.LinuxConfigurationArgs{ DisablePasswordAuthentication: pulumi.Bool(true), Ssh: &compute.SshConfigurationArgs{ PublicKeys: compute.SshPublicKeyTypeArray{ &compute.SshPublicKeyTypeArgs{ KeyData: sshKey.PublicKeyOpenssh, Path: pulumi.String(fmt.Sprintf("/home/%v/.ssh/authorized_keys", adminUsername)), }, }, }, }, }, StorageProfile: &compute.StorageProfileArgs{ OsDisk: &compute.OSDiskArgs{ Name: pulumi.String(fmt.Sprintf("%v-osdisk", vmName)), CreateOption: pulumi.String("FromImage"), }, ImageReference: &compute.ImageReferenceArgs{ Publisher: pulumi.String(osImagePublisher), Offer: pulumi.String(osImageOffer), Sku: pulumi.String(osImageSku), Version: pulumi.String(osImageVersion), }, }, }) if err != nil { return err }
// Once the machine is created, fetch its IP address and DNS hostname address := vm.ID().ApplyT(func(_ pulumi.ID) network.LookupPublicIPAddressResultOutput { return network.LookupPublicIPAddressOutput(ctx, network.LookupPublicIPAddressOutputArgs{ ResourceGroupName: resourceGroup.Name, PublicIpAddressName: publicIp.Name, }) })
// Export the VM's hostname, public IP address, HTTP URL, and SSH private key ctx.Export("ip", address.ApplyT(func(addr network.LookupPublicIPAddressResult) (string, error) { return *addr.IpAddress, nil }).(pulumi.StringOutput))
ctx.Export("hostname", address.ApplyT(func(addr network.LookupPublicIPAddressResult) (string, error) { return *addr.DnsSettings.Fqdn, nil }).(pulumi.StringOutput))
ctx.Export("url", address.ApplyT(func(addr network.LookupPublicIPAddressResult) (string, error) { return fmt.Sprintf("http://%s:%s", *addr.DnsSettings.Fqdn, servicePort), nil }).(pulumi.StringOutput))
ctx.Export("privatekey", sshKey.PrivateKeyOpenssh)
return nil })}using Pulumi;using AzureNative = Pulumi.AzureNative;using Random = Pulumi.Random;using System.Collections.Generic;using System;using System.Text;using Tls = Pulumi.Tls;
return await Pulumi.Deployment.RunAsync(() =>{ // Import the program's configuration settings. var config = new Pulumi.Config(); var vmName = config.Get("vmName") ?? "my-server"; var vmSize = config.Get("vmSize") ?? "Standard_A1_v2"; var osImage = config.Get("osImage") ?? "Debian:debian-12:12:latest"; var adminUsername = config.Get("adminUsername") ?? "pulumiuser"; var servicePort = config.Get("servicePort") ?? "80";
string[] osImageArgs = osImage.Split(":"); var osImagePublisher = osImageArgs[0]; var osImageOffer = osImageArgs[1]; var osImageSku = osImageArgs[2]; var osImageVersion = osImageArgs[3];
// Create an SSH key var sshKey = new Tls.PrivateKey("ssh-key", new() { Algorithm = "RSA", RsaBits = 4096, });
// Create a resource group var resourceGroup = new AzureNative.Resources.ResourceGroup("resource-group");
// Create a virtual network var virtualNetwork = new AzureNative.Network.VirtualNetwork("network", new() { ResourceGroupName = resourceGroup.Name, AddressSpace = new AzureNative.Network.Inputs.AddressSpaceArgs { AddressPrefixes = new[] { "10.0.0.0/16", }, }, Subnets = new[] { new AzureNative.Network.Inputs.SubnetArgs { Name = $"{vmName}-subnet", AddressPrefix = "10.0.1.0/24", }, }, });
// Use a random string to give the VM a unique DNS name var domainNameLabel = new Random.RandomString("domain-label", new() { Length = 8, Upper= false, Special = false, }).Result.Apply(result => $"{vmName}-{result}");
// Create a public IP address for the VM var publicIp = new AzureNative.Network.PublicIPAddress("public-ip", new() { ResourceGroupName = resourceGroup.Name, PublicIPAllocationMethod = AzureNative.Network.IPAllocationMethod.Dynamic, DnsSettings = new AzureNative.Network.Inputs.PublicIPAddressDnsSettingsArgs { DomainNameLabel = domainNameLabel, }, });
// Create a security group allowing inbound access over ports 80 (for HTTP) and 22 (for SSH) var securityGroup = new AzureNative.Network.NetworkSecurityGroup("security-group", new() { ResourceGroupName = resourceGroup.Name, SecurityRules = new[] { new AzureNative.Network.Inputs.SecurityRuleArgs { Name = $"{vmName}-securityrule", Priority = 1000, Direction = AzureNative.Network.SecurityRuleDirection.Inbound, Access = "Allow", Protocol = "Tcp", SourcePortRange = "*", SourceAddressPrefix = "*", DestinationAddressPrefix = "*", DestinationPortRanges = new[] { servicePort, "22", }, }, }, });
// Create a network interface with the virtual network, IP address, and security group var networkInterface = new AzureNative.Network.NetworkInterface("network-interface", new() { ResourceGroupName = resourceGroup.Name, NetworkSecurityGroup = new AzureNative.Network.Inputs.NetworkSecurityGroupArgs { Id = securityGroup.Id }, IpConfigurations = new[] { new AzureNative.Network.Inputs.NetworkInterfaceIPConfigurationArgs { Name = $"{vmName}-ipconfiguration", PrivateIPAllocationMethod = AzureNative.Network.IPAllocationMethod.Dynamic, Subnet = new AzureNative.Network.Inputs.SubnetArgs { Id = virtualNetwork.Subnets.GetAt(0).Apply(subnet => subnet.Id!) }, PublicIPAddress = new AzureNative.Network.Inputs.PublicIPAddressArgs { Id = publicIp.Id, }, } } });
// Define a script to be run when the VM starts up var initScript = $@"#!/bin/bash echo '<!DOCTYPE html> <html lang=""en""> <head> <meta charset=""utf-8""> <title>Hello, world!</title> </head> <body> <h1>Hello, world! 👋</h1> <p>Deployed with 💜 by <a href=""https://pulumi.com/"">Pulumi</a>.</p> </body> </html>' > index.html sudo python3 -m http.server {servicePort} &";
// Create the virtual machine var vm = new AzureNative.Compute.VirtualMachine("vm", new() { ResourceGroupName = resourceGroup.Name, NetworkProfile = new AzureNative.Compute.Inputs.NetworkProfileArgs { NetworkInterfaces = new[] { new AzureNative.Compute.Inputs.NetworkInterfaceReferenceArgs { Id = networkInterface.Id, Primary = true, }, }, }, HardwareProfile = new AzureNative.Compute.Inputs.HardwareProfileArgs { VmSize = vmSize, }, OsProfile = new AzureNative.Compute.Inputs.OSProfileArgs { ComputerName = vmName, AdminUsername = adminUsername, CustomData = Convert.ToBase64String(Encoding.UTF8.GetBytes(initScript)), LinuxConfiguration = new AzureNative.Compute.Inputs.LinuxConfigurationArgs { DisablePasswordAuthentication = true, Ssh = new AzureNative.Compute.Inputs.SshConfigurationArgs { PublicKeys = new[] { new AzureNative.Compute.Inputs.SshPublicKeyArgs { KeyData = sshKey.PublicKeyOpenssh, Path = $"/home/{adminUsername}/.ssh/authorized_keys", }, }, }, }, }, StorageProfile = new AzureNative.Compute.Inputs.StorageProfileArgs { OsDisk = new AzureNative.Compute.Inputs.OSDiskArgs { Name = $"{vmName}-osdisk", CreateOption = AzureNative.Compute.DiskCreateOptionTypes.FromImage, }, ImageReference = new AzureNative.Compute.Inputs.ImageReferenceArgs { Publisher = osImagePublisher, Offer = osImageOffer, Sku = osImageSku, Version = osImageVersion, }, }, });
// Once the machine is created, fetch its IP address and DNS hostname var vmAddress = vm.Id.Apply(_ => { return AzureNative.Network.GetPublicIPAddress.Invoke(new() { ResourceGroupName = resourceGroup.Name, PublicIpAddressName = publicIp.Name, }); });
// Export the VM's hostname, public IP address, HTTP URL, and SSH private key return new Dictionary<string, object?> { ["hostname"] = vmAddress.Apply(addr => addr.DnsSettings!.Fqdn), ["ip"] = vmAddress.Apply(addr => addr.IpAddress), ["url"] = vmAddress.Apply(addr => $"http://{addr.DnsSettings!.Fqdn}:{servicePort}"), ["privatekey"] = sshKey.PrivateKeyOpenssh, };});name: my-projectdescription: A Pulumi YAML program to deploy a virtual machine on Azureruntime: yaml
# Import the program's configuration settingsconfig: adminUsername: type: string default: pulumiuser vmName: type: string default: my-server vmSize: type: string default: Standard_A1_v2 osImage: type: string default: Debian:debian-12:12:latest servicePort: type: string default: "80"
variables: dnsName: ${vmName}-${random-string.result}
# Define a script to be run when the VM starts up initScript: fn::toBase64: | #!/bin/bash echo '<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> <h1>Hello, world! 👋</h1> <p>Deployed with 💜 by <a href="https://pulumi.com/">Pulumi</a>.</p> </body> </html>' > index.html sudo python3 -m http.server ${servicePort} & osImageArgs: fn::split: - ":" - ${osImage} osImagePublisher: ${osImageArgs[0]} osImageOffer: ${osImageArgs[1]} osImageSku: ${osImageArgs[2]} osImageVersion: ${osImageArgs[3]}
# Once the machine is created, fetch its IP address and DNS hostname address: fn::invoke: function: azure-native:network:getPublicIPAddress arguments: resourceGroupName: ${resource-group.name} publicIpAddressName: ${public-ip.name} options: dependsOn: - ${vm}
resources:
# Create an SSH key ssh-key: properties: algorithm: RSA rsaBits: 4096 type: tls:PrivateKey
# Create a resource group resource-group: type: azure-native:resources:ResourceGroup
# Create a virtual network network: type: azure-native:network:VirtualNetwork properties: resourceGroupName: ${resource-group.name} addressSpace: addressPrefixes: - 10.0.0.0/16 subnets: - name: default addressPrefix: 10.0.1.0/24
# Use a random string to give the VM a unique DNS name random-string: type: random:RandomString properties: length: 8 upper: false special: false
# Create a public IP address for the VM public-ip: type: azure-native:network:PublicIPAddress properties: resourceGroupName: ${resource-group.name} publicIPAllocationMethod: Dynamic dnsSettings: domainNameLabel: ${dnsName}
# Create a security group allowing inbound access over ports 80 (for HTTP) and 22 (for SSH) security-group: type: azure-native:network:NetworkSecurityGroup properties: resourceGroupName: ${resource-group.name} securityRules: - name: ${vmName}-securityrule priority: 1000 direction: Inbound access: Allow protocol: Tcp sourcePortRange: "*" sourceAddressPrefix: "*" destinationAddressPrefix: "*" destinationPortRanges: - fn::toJSON: ${servicePort} - "22"
# Create a network interface with the virtual network, IP address, and security group network-interface: type: azure-native:network:NetworkInterface properties: resourceGroupName: ${resource-group.name} networkSecurityGroup: id: ${security-group.id} ipConfigurations: - name: ${vmName}-ipconfiguration privateIPAllocationMethod: Dynamic subnet: id: ${network.subnets[0].id} publicIPAddress: id: ${public-ip.id}
# Create the virtual machine vm: type: azure-native:compute:VirtualMachine properties: resourceGroupName: ${resource-group.name} networkProfile: networkInterfaces: - id: ${network-interface.id} primary: true hardwareProfile: vmSize: ${vmSize} osProfile: computerName: ${vmName} adminUsername: ${adminUsername} customData: ${initScript} linuxConfiguration: disablePasswordAuthentication: true ssh: publicKeys: - keyData: ${ssh-key.publicKeyOpenssh} path: /home/${adminUsername}/.ssh/authorized_keys storageProfile: osDisk: name: ${vmName}-osdisk createOption: FromImage imageReference: publisher: ${osImagePublisher} offer: ${osImageOffer} sku: ${osImageSku} version: ${osImageVersion}
# Export the VM's hostname, public IP address, HTTP URL, and SSH private keyoutputs: ip: ${address.ipAddress} hostname: ${address.dnsSettings.fqdn} url: http://${address.dnsSettings.fqdn}:${servicePort} privatekey: ${ssh-key.privateKeyOpenssh}terraform { required_providers { azure-native = { source = "pulumi/azure-native" } tls = { source = "pulumi/tls" } random = { source = "pulumi/random" } }}
variable "admin_username" { description = "The user account to create on the VM" type = string default = "pulumiuser"}
variable "vm_name" { description = "The DNS hostname prefix to use for the VM" type = string default = "my-server"}
variable "vm_size" { description = "The machine size to use for the VM" type = string default = "Standard_A1_v2"}
variable "os_image" { description = "The Azure URN of the base image to use for the VM" type = string default = "Debian:debian-12:12:latest"}
variable "service_port" { description = "The HTTP service port to expose on the VM" type = number default = 80}
locals { os_image_parts = split(":", var.os_image) dns_name = "${var.vm_name}-${random_random_string.random-string.result}"
init_script = base64encode(<<-EOF #!/bin/bash echo '<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello, world!</title> </head> <body> <h1>Hello, world! 👋</h1> <p>Deployed with 💜 by <a href="https://pulumi.com/">Pulumi</a>.</p> </body> </html>' > index.html sudo python3 -m http.server ${var.service_port} & EOF )}
# Create an SSH keyresource "tls_private_key" "ssh-key" { algorithm = "RSA" rsa_bits = 4096}
# Use a random string to give the VM a unique DNS nameresource "random_random_string" "random-string" { length = 8 upper = false special = false}
# Create a resource groupresource "azure-native_resources_resource_group" "resource-group" {}
# Create a virtual networkresource "azure-native_network_virtual_network" "network" { resource_group_name = azure-native_resources_resource_group.resource-group.name address_space = { address_prefixes = ["10.0.0.0/16"] } subnets { name = "default" address_prefix = "10.0.1.0/24" }}
# Create a public IP address for the VMresource "azure-native_network_public_i_p_address" "public-ip" { resource_group_name = azure-native_resources_resource_group.resource-group.name public_ip_allocation_method = "Dynamic" dns_settings = { domain_name_label = local.dns_name }}
# Create a security group allowing inbound access over ports 80 (for HTTP) and 22 (for SSH)resource "azure-native_network_network_security_group" "security-group" { resource_group_name = azure-native_resources_resource_group.resource-group.name security_rules { name = "${var.vm_name}-securityrule" priority = 1000 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" source_address_prefix = "*" destination_address_prefix = "*" destination_port_ranges = [tostring(var.service_port), "22"] }}
# Create a network interface with the virtual network, IP address, and security groupresource "azure-native_network_network_interface" "network-interface" { resource_group_name = azure-native_resources_resource_group.resource-group.name network_security_group = { id = azure-native_network_network_security_group.security-group.id } ip_configurations { name = "${var.vm_name}-ipconfiguration" private_ip_allocation_method = "Dynamic" subnet = { id = azure-native_network_virtual_network.network.subnets[0].id } public_ip_address = { id = azure-native_network_public_i_p_address.public-ip.id } }}
# Create the virtual machineresource "azure-native_compute_virtual_machine" "vm" { resource_group_name = azure-native_resources_resource_group.resource-group.name network_profile = { network_interfaces = [{ id = azure-native_network_network_interface.network-interface.id primary = true }] } hardware_profile = { vm_size = var.vm_size } os_profile = { computer_name = var.vm_name admin_username = var.admin_username custom_data = local.init_script linux_configuration = { disable_password_authentication = true ssh = { public_keys = [{ key_data = tls_private_key.ssh-key.public_key_openssh path = "/home/${var.admin_username}/.ssh/authorized_keys" }] } } } storage_profile = { os_disk = { name = "${var.vm_name}-osdisk" create_option = "FromImage" } image_reference = { publisher = local.os_image_parts[0] offer = local.os_image_parts[1] sku = local.os_image_parts[2] version = local.os_image_parts[3] } }}
# Once the machine is created, fetch its IP address and DNS hostnamedata "azure-native_network_public_i_p_address" "address" { resource_group_name = azure-native_resources_resource_group.resource-group.name public_ip_address_name = azure-native_network_public_i_p_address.public-ip.name depends_on = [azure-native_compute_virtual_machine.vm]}
# Export the VM's hostname, public IP address, HTTP URL, and SSH private keyoutput "ip" { value = data.azure-native_network_public_i_p_address.address.ip_address}
output "hostname" { value = data.azure-native_network_public_i_p_address.address.dns_settings.fqdn}
output "url" { value = "http://${data.azure-native_network_public_i_p_address.address.dns_settings.fqdn}:${var.service_port}"}
output "private_key" { value = tls_private_key.ssh-key.private_key_openssh sensitive = true}Deploy the project#
Deploy with pulumi up. It starts with a preview of what it’s about to create and asks before it changes anything. Choose yes to deploy.
pulumi uppulumi upPulumi adds a random suffix to most resource names, so the resource group resource-group becomes something like resource-group8f3c2a1b. This is autonaming. It lets you deploy the same program to more than one stack, and lets Pulumi create a replacement before it deletes the old resource, without names colliding.
When it finishes, Pulumi prints the stack’s outputs, values it only knows once the resources exist:
- ip
The IP address of the virtual machine.
- hostname
The provider-assigned hostname of the virtual machine.
- url
The fully-qualified HTTP URL of the HTTP server running on the virtual machine.
- privatekey
The private key for SSH access to the VM. This value is a secret.
- ip
The IP address of the virtual machine.
- hostname
The provider-assigned hostname of the virtual machine.
- url
The fully-qualified HTTP URL of the HTTP server running on the virtual machine.
- privatekey
The private key for SSH access to the VM. This value is a secret.
- ip
The IP address of the virtual machine.
- hostname
The provider-assigned hostname of the virtual machine.
- url
The fully-qualified HTTP URL of the HTTP server running on the virtual machine.
- privatekey
The private key for SSH access to the VM. This value is a secret.
- ip
The IP address of the virtual machine.
- hostname
The provider-assigned hostname of the virtual machine.
- url
The fully-qualified HTTP URL of the HTTP server running on the virtual machine.
- privatekey
The private key for SSH access to the VM. This value is a secret.
- ip
The IP address of the virtual machine.
- hostname
The provider-assigned hostname of the virtual machine.
- url
The fully-qualified HTTP URL of the HTTP server running on the virtual machine.
- privatekey
The private key for SSH access to the VM. This value is a secret.
- ip
The IP address of the virtual machine.
- hostname
The provider-assigned hostname of the virtual machine.
- url
The fully-qualified HTTP URL of the HTTP server running on the virtual machine.
- private_key
The private key for SSH access to the VM. This value is a secret.
Read one any time with pulumi stack output <name>, or pass it to another command. This opens the VM’s web page in your browser:
open $(pulumi stack output url)xdg-open $(pulumi stack output url)Start-Process $(pulumi stack output url)To check it from the terminal, fetch the page with curl. It prints the template’s placeholder page, which says Hello, world!. The web server starts as the VM boots, so if curl can’t connect, wait a minute and try again:
curl $(pulumi stack output url)curl.exe $(pulumi stack output url)Need help?
If pulumi up asks you to run az login, or says your sign-in has expired, sign in again:
az loginaz loginStill stuck? Let us know.
Customize the project#
Projects created with this template expose the following configuration settings:
- adminUsername
The user account to create on the VM. Defaults to
pulumiuser.- vmName
The DNS hostname prefix to use for the VM. Defaults to
my-server.- vmSize
The machine size to use for the VM. Defaults to
Standard_A1_v2.- osImage
The Azure URN of the base image to use for the VM. Defaults to
Debian:debian-12:12:latest.- servicePort
The HTTP service port to expose on the VM. Defaults to
80.
- adminUsername
The user account to create on the VM. Defaults to
pulumiuser.- vmName
The DNS hostname prefix to use for the VM. Defaults to
my-server.- vmSize
The machine size to use for the VM. Defaults to
Standard_A1_v2.- osImage
The Azure URN of the base image to use for the VM. Defaults to
Debian:debian-12:12:latest.- servicePort
The HTTP service port to expose on the VM. Defaults to
80.
- adminUsername
The user account to create on the VM. Defaults to
pulumiuser.- vmName
The DNS hostname prefix to use for the VM. Defaults to
my-server.- vmSize
The machine size to use for the VM. Defaults to
Standard_A1_v2.- osImage
The Azure URN of the base image to use for the VM. Defaults to
Debian:debian-12:12:latest.- servicePort
The HTTP service port to expose on the VM. Defaults to
80.
- adminUsername
The user account to create on the VM. Defaults to
pulumiuser.- vmName
The DNS hostname prefix to use for the VM. Defaults to
my-server.- vmSize
The machine size to use for the VM. Defaults to
Standard_A1_v2.- osImage
The Azure URN of the base image to use for the VM. Defaults to
Debian:debian-12:12:latest.- servicePort
The HTTP service port to expose on the VM. Defaults to
80.
- adminUsername
The user account to create on the VM. Defaults to
pulumiuser.- vmName
The DNS hostname prefix to use for the VM. Defaults to
my-server.- vmSize
The machine size to use for the VM. Defaults to
Standard_A1_v2.- osImage
The Azure URN of the base image to use for the VM. Defaults to
Debian:debian-12:12:latest.- servicePort
The HTTP service port to expose on the VM. Defaults to
80.
- admin_username
The user account to create on the VM. Defaults to
pulumiuser.- vm_name
The DNS hostname prefix to use for the VM. Defaults to
my-server.- vm_size
The machine size to use for the VM. Defaults to
Standard_A1_v2.- os_image
The Azure image reference (publisher:offer:sku:version) to use for the VM. Defaults to
Debian:debian-12:12:latest.- service_port
The HTTP service port to expose on the VM. Defaults to
80.
All of these settings are optional and may be adjusted either by editing the stack configuration file directly (by default, Pulumi.dev.yaml) or by changing their values with pulumi config set.
Adding a tag#
Tags are key-value labels you can search and filter resources by. Add an Environment tag to the VM in index.tsindex.js__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yamlmain.tf:
const vm = new compute.VirtualMachine("vm", { resourceGroupName: resourceGroup.name, tags: { Environment: "dev", }, // ...vm = compute.VirtualMachine( "vm", resource_group_name=resource_group.name, tags={ "Environment": "dev", }, # ...vm, err := compute.NewVirtualMachine(ctx, "vm", &compute.VirtualMachineArgs{ ResourceGroupName: resourceGroup.Name, Tags: pulumi.StringMap{ "Environment": pulumi.String("dev"), }, // ...var vm = new AzureNative.Compute.VirtualMachine("vm", new(){ ResourceGroupName = resourceGroup.Name, Tags = { { "Environment", "dev" }, }, // ...vm: type: azure-native:compute:VirtualMachine properties: resourceGroupName: ${resource-group.name} tags: Environment: dev # ...resource "azure-native_compute_virtual_machine" "vm" { resource_group_name = azure-native_resources_resource_group.resource-group.name tags = { Environment = "dev" } # ...Deploy it. The preview shows the VM as an update. Azure adds tags without replacing or restarting it, so url keeps working. Then list your VMs with their tags; the new one shows "Environment": "dev":
pulumi upaz vm list --query "[].{name:name, tags:tags}"pulumi upaz vm list --query "[].{name:name, tags:tags}"To see what else you can add, browse the Azure Native provider in the Pulumi Registry.
Cleaning up#
You can cleanly destroy the stack and all of its infrastructure with pulumi destroy:
pulumi destroypulumi destroyLearn more#
- Browse other architecture templates in the Templates gallery.
- Explore the Azure Native provider API docs in the Pulumi Registry.
- Walk through Pulumi from the ground up in Pulumi Tutorials.
- Read the latest Azure posts on the Pulumi blog.