Azure Terraform - Create a virtual machine and enable SSH and ping
Introduction
Explanation of the terraform yaml
file
Previewing, applying infrastructure changes with terraform
Connecting to the virtual machine via ssh
and using ping
Deleting created infrastructure
The reference .tf
Terraform configuration file
Introduction
In this tutorial we will deploy a Linux VM to Azure cloud, using terraform.
The virtual machine will have a private and public IP, and we will enable remote login via ssh
with a public/private keypair. We will also enable ping.
The first step is to create the .tf
terraform file, which is yaml
syntax.
There should be a single .tf
file in the working directory.
Explanation of the terraform yaml
file
Let’s walk through the content of the file, which is at the end of this article.
The AzureRM Terraform Provider is called by the initialisation sequence; it allows managing resources within Azure Resource Manager. Its module is hashicorp/azurerm
.
We will then, within the location
group, choose uksouth
as the deployment location.
The resource group defined in the block resource_group_name
will be named debian-vm-rg
. An Azure Resource Group
is a logical container that holds related Azure resources
(VMs, storage accounts, databases, and networking components).
To log into the Linux machine, we will define admin_username
with the account name of azureuser
.
We will use a ssh
keypair to log into the VM; we will generate it via the ssh-keygen
command. It is best to specify the type as rsa
, as this is the only type accepted by Azure for now. You can add a passphrase to the key, it will strengthen its security.
george@fedora:~$ ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa
Generating public/private rsa key pair.
Enter passphrase for "/home/george/.ssh/id_rsa" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/george/.ssh/id_rsa1
Your public key has been saved in /home/george/.ssh/id_rsa1.pub
The key fingerprint is:
SHA256:bXuY4jPIELXEv/cwd4JEXgZoXFZXWtVqs06/xxx george@host
The key's randomart image is:
+---[RSA 4096]----+
| . . o+o. ..O|
| + +.. o. +E|
| o + o o ...|
| . . ..o +. |
| . Soo. ..o.|
| . ..=+o.+.o|
| o ...+=.=.oo|
| o.o. .. ..+|
| .o .o|
+----[SHA256]-----+
The vm_size
variable will instantiate a virtual machine of the size we specify. Standard_B1s
. It is a low-cost burstable VM size in Azure, ideal for workloads that don’t require sustained CPU usage but need occasional performance boosts.
It has:
- 1 vCPU
- 1 GiB RAM
- Burstable CPU performance (extra power when needed)
- Suitable for testing, lightweight workloads, and small web apps
- Cost-effective with lower pricing than standard VM sizes
We will then specify the Azure Virtual Network
resource called azurerm_virtual_network
.
Azure Virtual Network (VNet
) is Microsoft Azure’s networking service that allows you to create private networks for your resources. It enables secure communication between Azure services, virtual machines, and even on-premises infrastructure.
It provides:
- Isolated Networks – Each
VNet
is logically isolated, ensuring secure communication. - Custom Subnets – You can divide your
VNet
into subnets for better organisation. - Network Security Groups (
NSG
) – Control inbound and outbound traffic at the subnet or VM level. The IP address space for it is10.0.0.0/16
, meaning we can have $2^{16}$=65535 IP addresses. The network mask is/16
, meaning that we can use the last two full bytes onlyx, y
-10.0.x.y
.
Within the azurerm_subnet
VNet
we will create a subnet called azurerm_subnet
, with an IP range of 10.0.2.0/24
.
We can have 254 host IPs allocated to VMs, as per below, since the subnet mask is /24
, meaning we have 8 bits ($2^{8}$=256) available to allocate IPs. The first (.0
) and the last IP (.255
) are reserved for the network ID and for broadcast to the entire subnet.
We can use ipcalc
to calculate how many usable host IPs there are in a subnet:
george@fedora:~$ ipcalc 10.0.2.0/24
Network: 10.0.2.0/24
Netmask: 255.255.255.0 = 24
Broadcast: 10.0.2.255
Address space: Private Use
HostMin: 10.0.2.1
HostMax: 10.0.2.254
Hosts/Net: 254
A private IP address will be allocated from the IP range of 10.0.2.0/24
.
The resource azurerm_public_ip
will allocate a public IP address, so that we can connect to the machine via internet; the private IP address cannot be accessed publicly. The public IP will be static and associated with a hostname, will not change for the lifetime of the VM. This helps in production, to properly access resources such as email/database servers. A dynamic IP changes if a server is stopped and started after maintenance, and this would disrupt other computers accessing our machine, who expect the same IP/hostname pair.
We will specify the azurerm_network_security_group
NSG
. This network security group will have two inbound rules (from machines from the internet) to allow:
- inbound ping to the VM (useful to know if the machine is up)
ssh
connection to the machine via port22
.
We will specifyazurerm_network_interface
as a network card, whoseip_configuration
section specifies the use of dynamic private IP.
Theazurerm_linux_virtual_machine
resource will instantiate the VM with the size, in the location and with the user account we specified earlier.
We will be usingdebian-12
as the operating system.
Theos_disk
resource will specify a disk with the size of30 GB
.
TheStandard_LRS
disk type refers to Standard Locally Redundant Storage (LRS) where your data is stored three times within a single Azure data centre.
Theadmin_ssh_key
variable will use thessh
key we specified earlier.
The last two code blocks will output the self-explanatory variablespublic_ip_address
andadmin_username
.
Previewing, applying infrastructure changes with terraform
To create all the resources specified in the terraform file, we will run three commands.
The first, terraform init
, initialises the working directory containing Terraform configurations:
- downloads provider plugins – (Azure, AWS, or Google Cloud) based on the
.tf
configuration file. - sets up backend configuration – if using remote state storage (e.g., Azure Storage, AWS S3), it configures how Terraform will store state files.
- prepares the workspace – it creates a
.terraform
directory where Terraform maintains metadata about plugins, modules, and the state backend. - validates the configuration – ensures all required dependencies are correctly installed before moving forward.
The next command, terraform plan
, is used to preview the changes Terraform will apply to the infrastructure before actually making them. It helps you understand what will change without modifying any resources.
The command terraform apply
executes the infrastructure changes that were previewed in terraform plan
. It creates, updates, or destroys resources based on the Terraform configuration files.
To skip the confirmation prompt, use:
terraform apply -auto-approve
Here is the output of this command:
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_linux_virtual_machine.main will be created
+ resource "azurerm_linux_virtual_machine" "main" {
+ admin_username = "azureuser"
+ allow_extension_operations = true
+ bypass_platform_safety_checks_on_user_schedule_enabled = false
+ computer_name = (known after apply)
+ disable_password_authentication = true
+ disk_controller_type = (known after apply)
+ extensions_time_budget = "PT1H30M"
[..]
+ admin_ssh_key {
+ public_key = "ssh-rsa xxx= george@host"
+ username = "azureuser"
}
+ os_disk {
+ caching = "ReadWrite"
+ disk_size_gb = 30
+ name = (known after apply)
+ storage_account_type = "Standard_LRS"
+ write_accelerator_enabled = false
}
+ source_image_reference {
+ offer = "debian-12"
+ publisher = "Debian"
+ sku = "12-gen2"
+ version = "latest"
}
[..]
# azurerm_public_ip.main will be created
+ resource "azurerm_public_ip" "main" {
+ allocation_method = "Static"
+ ddos_protection_mode = "VirtualNetworkInherited"
+ fqdn = (known after apply)
+ id = (known after apply)
+ idle_timeout_in_minutes = 4
+ ip_address = (known after apply)
+ ip_version = "IPv4"
+ location = "uksouth"
+ name = "debian12-vm-pip"
+ resource_group_name = "debian-vm-rg"
+ sku = "Standard"
+ sku_tier = "Regional"
}
When terraform apply
completes successfully, it will output the user name and public IP that we can use to connect to the machine via ssh
:
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
admin_username = "azureuser"
public_ip_address = "x.y.z.t"
Connecting to the virtual machine via ssh
and using ping
Let’s connect via ssh
, with the id_rsa
keypair we created earlier:
ssh -i .ssh/id_rsa azureuser@x.y.z.t
We can also see if the VM is up, by using ping
:
$ ping x.y.z.t
PING x.y.z.t (x.y.z.t) 56(84) bytes of data.
64 bytes from x.y.z.t: icmp_seq=1 ttl=47 time=40.0 ms
Deleting created infrastructure
To remove all resources we have created, we run terraform destroy
:
azurerm_resource_group.main: Refreshing state... [id=/subscriptions/xxx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg]
azurerm_virtual_network.main: Refreshing state... [id=/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Network/virtualNetworks/debian12-vm-vnet]
[..]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# azurerm_linux_virtual_machine.main will be destroyed
- resource "azurerm_linux_virtual_machine" "main" {
- admin_username = "azureuser" -> null
- allow_extension_operations = true -> null
- bypass_platform_safety_checks_on_user_schedule_enabled = false -> null
- computer_name = "debian12-vm" -> null
- disable_password_authentication = true -> null
- disk_controller_type = "SCSI" -> null
- encryption_at_host_enabled = false -> null
- extensions_time_budget = "PT1H30M" -> null
- id = "/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Compute/virtualMachines/debian12-vm" -> null
- location = "uksouth" -> null
- max_bid_price = -1 -> null
- name = "debian12-vm" -> null
- network_interface_ids = [
- "/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Network/networkInterfaces/debian12-vm-nic",
] -> null
- patch_assessment_mode = "ImageDefault" -> null
- patch_mode = "ImageDefault" -> null
- platform_fault_domain = -1 -> null
- priority = "Regular" -> null
- private_ip_address = "10.0.2.4" -> null
- private_ip_addresses = [
- "10.0.2.4",
] -> null
- provision_vm_agent = true -> null
- public_ip_address = "x.y.z.t" -> null
- public_ip_addresses = [
- "x.y.z.t",
] -> null
- resource_group_name = "debian-vm-rg" -> null
- secure_boot_enabled = false -> null
- size = "Standard_B1s" -> null
- tags = {} -> null
- virtual_machine_id = "xxx-8bd8-45e3-a2ca-851381933651" -> null
- vm_agent_platform_updates_enabled = false -> null
- vtpm_enabled = false -> null
# (13 unchanged attributes hidden)
- admin_ssh_key {
- public_key = "ssh-rsa xxx= george@host" -> null
- username = "azureuser" -> null
}
- os_disk {
- caching = "ReadWrite" -> null
- disk_size_gb = 30 -> null
- name = "debian12-vm_OsDisk_1_6d9d5c0e7d574xxx" -> null
- storage_account_type = "Standard_LRS" -> null
- write_accelerator_enabled = false -> null
# (3 unchanged attributes hidden)
}
- source_image_reference {
- offer = "debian-12" -> null
- publisher = "Debian" -> null
- sku = "12-gen2" -> null
- version = "latest" -> null
}
}
[..]
We will need to confirm with yes
:
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
azurerm_network_interface_security_group_association.main: Destroying... [id=/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Network/networkInterfaces/debian12-vm-nic|/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Network/networkSecurityGroups/debian12-vm-nsg]
azurerm_linux_virtual_machine.main: Destroying... [id=/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Compute/virtualMachines/debian12-vm]
azurerm_network_interface_security_group_association.main: Destruction complete after 2s
azurerm_network_security_group.main: Destroying... [id=/subscriptions/xx-c84d-459e-bc17-xx/resourceGroups/debian-vm-rg/providers/Microsoft.Network/networkSecurityGroups/debian12-vm-nsg]
azurerm_linux_virtual_machine.main: Still destroying... [id=/subscriptions/xx-c84d-459e-bc17-...ft.Compute/virtualMachines/debian12-vm, 10s elapsed]
azurerm_resource_group.main: Destruction complete after 16s
Destroy complete! Resources: 8 destroyed.
The reference .tf
Terraform configuration file
The complete yaml
file is below, save it as main.tf
or similar.
# Configure the AzureRM Provider
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
# Using ~> 3.0 as it seemed to work with your environment and lock file after upgrade
version = "~> 3.0"
}
}
required_version = ">= 1.0.0"
}
provider "azurerm" {
features {}
}
# --- Variables ---
variable "location" {
description = "Azure region for deployment"
type = string
default = "uksouth" # Defaulting to UK South
}
variable "resource_group_name" {
description = "Name of the resource group"
type = string
default = "debian-vm-rg" # Default resource group name
}
variable "vm_name" {
description = "Name of the Virtual Machine"
type = string
default = "debian12-vm" # Default VM name
}
variable "admin_username" {
description = "Username for the VM administrator account"
type = string
default = "azureuser" # Default admin username
}
variable "ssh_public_key" {
description = "SSH public key to use for VM authentication"
type = string
# Setting the default value directly in the file using your specific key
default = "ssh-rsa xxx= george@host"
}
variable "vm_size" {
description = "Size of the Virtual Machine"
type = string
default = "Standard_B1s" # Smallest instance size
}
# --- Resources ---
# Create a Resource Group
resource "azurerm_resource_group" "main" {
name = var.resource_group_name
location = var.location
}
# Create a Virtual Network
resource "azurerm_virtual_network" "main" {
name = "${var.vm_name}-vnet"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
address_space = ["10.0.0.0/16"]
}
# Create a Subnet
resource "azurerm_subnet" "internal" {
name = "internal"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.2.0/24"]
}
# Create a Public IP Address
resource "azurerm_public_ip" "main" {
name = "${var.vm_name}-pip"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Static"
sku = "Standard" # Standard SKU is recommended
}
# Create a Network Security Group (NSG)
resource "azurerm_network_security_group" "main" {
name = "${var.vm_name}-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
# Allow SSH (TCP port 22)
security_rule {
name = "AllowSSH"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "Internet" # Consider restricting this to specific IPs
destination_address_prefix = "*"
}
# Allow Ping (ICMP)
security_rule {
name = "AllowPing"
priority = 110 # Must have a different priority
direction = "Inbound"
access = "Allow"
protocol = "Icmp"
source_port_range = "*"
destination_port_range = "*" # ICMP doesn't use destination ports
source_address_prefix = "Internet" # Consider restricting this to specific IPs
destination_address_prefix = "*"
}
}
# Create a Network Interface (NIC)
resource "azurerm_network_interface" "main" {
name = "${var.vm_name}-nic"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.internal.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.main.id
}
# NSG Association is done using a separate resource (see below)
}
# Associate the Network Interface with the Network Security Group
resource "azurerm_network_interface_security_group_association" "main" {
network_interface_id = azurerm_network_interface.main.id
network_security_group_id = azurerm_network_security_group.main.id
}
# Create the Linux Virtual Machine
resource "azurerm_linux_virtual_machine" "main" {
name = var.vm_name
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
size = var.vm_size # Use the variable VM size (Standard_B1s)
admin_username = var.admin_username
network_interface_ids = [azurerm_network_interface.main.id] # Link the VM to the NIC
# Configure the OS image - Debian 12 (Bookworm)
source_image_reference {
publisher = "Debian"
offer = "debian-12"
sku = "12-gen2" # Use the Gen2 SKU (ensure VM size supports Gen2)
version = "latest"
}
# Configure the OS disk
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS" # Cheapest storage option
disk_size_gb = 30 # 30 GB disk size
}
# Configure SSH access using the public key - Relying on implicit injection by Azure/VMAccess during creation
admin_ssh_key {
username = var.admin_username
public_key = var.ssh_public_key # Use the variable SSH public key
}
# Optional: Enable password authentication (less secure - use SSH keys instead)
# disable_password_authentication = false
# Optional: Boot Diagnostics (useful for troubleshooting boot issues)
# boot_diagnostics {
# storage_account_uri = "YOUR_STORAGE_ACCOUNT_BLOB_ENDPOINT" # Requires a storage account
# }
}
# --- Removed the explicit VM Access Extension resource and the data block ---
# They are not included in this version to avoid the "Invalid data source" error.
# --- Outputs ---
# Output the Public IP address
output "public_ip_address" {
description = "The public IP address of the Virtual Machine"
value = azurerm_public_ip.main.ip_address
}
# Output the Admin Username
output "admin_username" {
description = "The admin username for the Virtual Machine"
value = azurerm_linux_virtual_machine.main.admin_username
}