Skip to main content

Search the Dev Center

Search tutorials, templates, examples, and more.

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.

Architecture diagram of the Azure Virtual Machine template Azure Azure Virtual Network Subnet Security Group Virtual Machine

Before you begin#

For this walkthrough, you'll need:

Create a new project#

Create a new project from the template:

Terminal window
mkdir my-virtual-machine; cd my-virtual-machine
pulumi new vm-azure-typescript

The 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:

KeyDescriptionDefault
azure-native:locationThe Azure location to deploy intoWestUS2
adminUsernameThe user account to create on the VMpulumiuser
vmNameThe DNS hostname prefix to use for the VMmy-server
vmSizeThe machine size to use for the VMStandard_A1_v2
osImageThe Azure URN of the base image to use for the VMDebian:debian-12:12:latest
servicePortThe HTTP service port to expose on the VM80
You can change values like these later with pulumi config set :
Terminal window
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>

Review the code#

When setup completes, you’ll see a few files:

  • Pulumi.yaml contains the project name and the language it’s written in.
  • Pulumi.dev.yaml contains the dev stack’s settings, including the values you just entered.
  • index.ts contains 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.ts:

index.ts
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 settings
const 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 key
const sshKey = new tls.PrivateKey("ssh-key", {
algorithm: "RSA",
rsaBits: 4096,
});
// Create a resource group
const resourceGroup = new resources.ResourceGroup("resource-group");
// Create a virtual network
const 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 name
var domainNameLabel = new random.RandomString("domain-label", {
length: 8,
upper: false,
special: false,
}).result.apply(result => `${vmName}-${result}`);
// Create a public IP address for the VM
const 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 group
const 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 up
const 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
const 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 hostname
const 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 key
export 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;

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.

Terminal window
pulumi up

Pulumi 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.

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:

Terminal window
open $(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:

Terminal window
curl $(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:

Terminal window
az login

Still 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.

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.ts:

const vm = new compute.VirtualMachine("vm", {
resourceGroupName: resourceGroup.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":

Terminal window
pulumi up
az vm list --query "[].{name:name, tags:tags}"

Cleaning up#

You can cleanly destroy the stack and all of its infrastructure with pulumi destroy:

Terminal window
pulumi destroy

Learn more#

Related

The infrastructure as code platform for any cloud.