How to create a virtual machine using Terraform on Azure Stack Hub
This document explains how to create a VM using the azurestack Terraform provider with Service Principal Name authentication.
Prerequisites
Prerequisites from a Windows-based external client.
-
-
Terraform executable
-
- Download Terraform
-
An active Azure Stack Hub Subscription (required to create SPN if you want to use the same SPN for both Azure and Azure Stack Hub)
-
Service Principal Name
HashiCorp documentation
-
-
-
How to use Terraform with Azure Stack Hub
Authentication
In order to authenticate with Terraform you will need to have a valid Service Principal Name (SPN) - how to create one is described in the Prerequisites.
Note
You only need your Service Principal Name (SPN) to be assigned to your Azure Stack Hub subscription, despite what official Terraform documentation says.
The process of authentication can be handled in one of two ways, either as Environment Variables or in the Provider Block.
-
- Environment Variables
- Provider Block
You can create your Terraform plan by putting only the plan itself into main.tf
and then keeping variables.tf
separate. The actual values are declared in the terraform.tfvars
file, such as the SPN credentials.
Example of how variables are used in Terraform
variables.tf
variable "arm_endpoint" {}
variable "subscription_id" {}
variable "client_id" {}
variable "client_secret" {}
variable "tenant_id" {}
Note
You can also put the content of variables.tf
at the top of the main.tf
file.
main.tf
provider "azurestack" {
arm_endpoint = "${var.arm_endpoint}"
subscription_id = "${var.subscription_id}"
client_id = "${var.client_id}"
client_secret = "${var.client_secret}"
tenant_id = "${var.tenant_id}"
}
terraform.tfvars
# Configure the Azure Stack Hub Provider
arm_endpoint = "https://management.{region}.{domain}"
subscription_id = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
client_id = "{applicationId}"
client_secret = "{applicationSecret}"
tenant_id = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
You can create your Terraform plan by putting everything in one main.tf
file, which then contains your Provider and variables settings explicitly in said plan.
main.tf
# Configure the Azure Stack Hub Provider
provider "azurestack" {
arm_endpoint = "https://management.{region}.{domain}"
subscription_id = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
client_id = "{applicationId}"
client_secret = "{applicationPassword}"
tenant_id = "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
}
Rest of the file (...)
Argument reference
-
-
arm_endpoint
- The Azure Resource Manager API Endpoint for your Azure Stack Hub instance. This will be https://management.{region}.{domain}
.
Note
For UKCloud, the arm_endpoint is https://management.frn00006.azure.ukcloud.com
.
-
-
subscription_id
- The ID of your Azure Stack Hub Subscription.
-
client_id
- The Application GUID that you configured your Service Principal Name (SPN) to use.
-
client_secret
- The Application password that you configured your Service Principal Name (SPN) to use.
-
tenant_id
- The tenant ID of your Azure Active Directory tenant domain. It can either be the actual GUID or your Azure Active Directory tenant domain name.
Official Variables Guide
Creating a VM
The examples that follow show how to create VMs using Terraform. The code changes depending on whether you are creating a VM with an unmanaged or managed disk. You can select the type of disk you want to use, which will then update the examples below with the appropriate code.
Declare variables
Variable Name | Variable Description | Input |
---|---|---|
arm_endpoint | The Azure Resource Manager API endpoint for your Azure Stack Hub instance. This will be https://management.{region}.{domain} . |
|
subscription_id | The ID of your Azure Stack Hub subscription. | |
client_id | The application GUID that you configured your Service Principal Name (SPN) to use. | |
client_secret | The application password that you have configured your Service Principal Name (SPN) to use. | |
rg_name | The name of the resource group you want to create | |
tenant_id | The tenant ID of your Azure Active Directory tenant domain. It can either be the actual GUID or your Azure Active Directory tenant domain name. | |
location | The name of the Azure/Azure Stack Hub region. | |
vm_count | The number of VMs you want to create | |
vm_username | The username you want to assign to the VM | |
vm_password | The password you want to assign to the VM | |
vm_image | The operating system you want to use | |
instance_size | Size of the virtual machine to be created | |
rg_tag | An optional tag to help categorize the resource group, e.g. "Production" | |
Important
This example is only the main.tf
file. You will still need the variables.tf
and terraform.tfvars
files as shown in the following section.
Defining the required Azure Stack Hub resources in the main.tf
file
Tip
Resource attributes such as "name" can be hardcoded as opposed to using variables from the external files for the declaration.
-
- Windows VM with Managed Disks
- Windows VM with Unmanaged Disks
- Linux VM with Managed Disks
- Linux VM with Unmanaged Disks
provider "azurestack" {
arm_endpoint = "${var.arm_endpoint}"
subscription_id = "${var.subscription_id}"
client_id = "${var.client_id}"
client_secret = "${var.client_secret}"
tenant_id = "${var.tenant_id}"
}
# Create a resource group
resource "azurestack_resource_group" "rg" {
name = "${var.rg_name}"
tags = {
environment = "${var.rg_tag}"
}
}
resource "azurestack_network_security_group" "nsg" {
name = "${azurestack_resource_group.rg.name}-SecurityGroup"
resource_group_name = "${azurestack_resource_group.rg.name}"
security_rule {
name = "rdp"
priority = "100"
direction = "Inbound"
access = "Allow"
protocol = "tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_virtual_network" "virtual-network" {
name = "${azurestack_resource_group.rg.name}-VirtualNetwork"
address_space = ["10.0.0.0/16"]
resource_group_name = "${azurestack_resource_group.rg.name}"
}
resource "azurestack_subnet" "subnet1" {
name = "${azurestack_resource_group.rg.name}-Subnet1"
resource_group_name = "${azurestack_resource_group.rg.name}"
virtual_network_name = "${azurestack_virtual_network.virtual-network.name}"
address_prefix = "10.0.2.0/24"
network_security_group_id = "${azurestack_network_security_group.nsg.id}"
}
resource "azurestack_public_ip" "public-ip" {
count = "${var.vm_count}"
name = "public-ip-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
public_ip_address_al}
resource "azurestack_network_interface" "nic" {
name = "${azurestack_resource_group.rg.name}-NIC${count.index + 1}"
count = "${var.vm_count}"
resource_group_name = "${azurestack_resource_group.rg.name}"
ip_configuration {
name = "nic-ip-config1"
subnet_id = "${azurestack_subnet.subnet1.id}"
private_ip_address_alpublic_ip_address_id = "${element(azurestack_public_ip.public-ip.*.id, count.index)}"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_virtual_machine" "vm" {
count = "${var.vm_count}"
name = "vm-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
network_interface_ids = ["${element(azurestack_network_interface.nic.*.id, count.index)}"]
vm_size = "${var.vm_size}"
# Uncomment this line to delete the OS disk automatically when deleting the VM
# delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM
# delete_data_disks_on_termination = true
storage_image_reference {
publisher = "${element(split("/", var.vm_image_string), 0)}"
offer = "${element(split("/", var.vm_image_string), 1)}"
sku = "${element(split("/", var.vm_image_string), 2)}"
version = "${element(split("/", var.vm_image_string), 3)}"
}
storage_os_disk {
name = "vm-${count.index + 1}-OS-Disk"
caching = "ReadWrite"
managed_disk_type = "Standard_LRS"
create_option = "FromImage"
}
# Optional data disks
storage_data_disk {
name = "vm-${count.index + 1}-Data-Disk"
disk_size_gb = "100"
managed_disk_type = "Standard_LRS"
create_option = "Empty"
lun = 0
}
os_profile {
computer_name = "host${count.index + 1}"
admin_username = "${var.admin_username}"
admin_password = "${var.admin_password}"
}
os_profile_windows_config {
}
tags = "${azurestack_resource_group.rg.tags}"
}
provider "azurestack" {
arm_endpoint = "${var.arm_endpoint}"
subscription_id = "${var.subscription_id}"
client_id = "${var.client_id}"
client_secret = "${var.client_secret}"
tenant_id = "${var.tenant_id}"
}
# Create a resource group
resource "azurestack_resource_group" "rg" {
name = "${var.rg_name}"
tags = {
environment = "${var.rg_tag}"
}
}
resource "azurestack_network_security_group" "nsg" {
name = "${azurestack_resource_group.rg.name}-SecurityGroup"
resource_group_name = "${azurestack_resource_group.rg.name}"
security_rule {
name = "rdp"
priority = "100"
direction = "Inbound"
access = "Allow"
protocol = "tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_virtual_network" "virtual-network" {
name = "${azurestack_resource_group.rg.name}-VirtualNetwork"
address_space = ["10.0.0.0/16"]
resource_group_name = "${azurestack_resource_group.rg.name}"
}
resource "azurestack_subnet" "subnet1" {
name = "${azurestack_resource_group.rg.name}-Subnet1"
resource_group_name = "${azurestack_resource_group.rg.name}"
virtual_network_name = "${azurestack_virtual_network.virtual-network.name}"
address_prefix = "10.0.2.0/24"
network_security_group_id = "${azurestack_network_security_group.nsg.id}"
}
resource "azurestack_public_ip" "public-ip" {
count = "${var.vm_count}"
name = "public-ip-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
public_ip_address_al}
resource "azurestack_network_interface" "nic" {
name = "${azurestack_resource_group.rg.name}-NIC${count.index + 1}"
count = "${var.vm_count}"
resource_group_name = "${azurestack_resource_group.rg.name}"
ip_configuration {
name = "nic-ip-config1"
subnet_id = "${azurestack_subnet.subnet1.id}"
private_ip_address_alpublic_ip_address_id = "${element(azurestack_public_ip.public-ip.*.id, count.index)}"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_storage_account" "storage-account" {
count = "${var.vm_count}"
name = "${azurestack_resource_group.rg.name}accsa${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
account_tier = "Standard"
account_replication_type = "LRS"
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_storage_container" "storage-container" {
count = "${var.vm_count}"
name = "vhds${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
storage_account_name = "${element(azurestack_storage_account.storage-account.*.name, count.index)}"
container_access_type = "private"
}
resource "azurestack_virtual_machine" "vm" {
count = "${var.vm_count}"
name = "vm-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
network_interface_ids = ["${element(azurestack_network_interface.nic.*.id, count.index)}"]
vm_size = "${var.vm_size}"
# Uncomment this line to delete the OS disk automatically when deleting the VM
# delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM
# delete_data_disks_on_termination = true
storage_image_reference {
publisher = "${element(split("/", var.vm_image_string), 0)}"
offer = "${element(split("/", var.vm_image_string), 1)}"
sku = "${element(split("/", var.vm_image_string), 2)}"
version = "${element(split("/", var.vm_image_string), 3)}"
}
storage_os_disk {
name = "vm-${count.index + 1}-OS-Disk"
vhd_uri = "${element(azurestack_storage_account.storage-account.*.primary_blob_endpoint, count.index)}${element(azurestack_storage_container.storage-container.*.name, count.index)}/vm-${count.index + 1}-osdisk.vhd"
caching = "ReadWrite"
create_option = "FromImage"
}
# Optional data disks
storage_data_disk {
name = "vm-${count.index + 1}-Data-Disk"
vhd_uri = "${element(azurestack_storage_account.storage-account.*.primary_blob_endpoint, count.index)}${element(azurestack_storage_container.storage-container.*.name, count.index)}/vm-${count.index} + 1}-datadisk.vhd"
disk_size_gb = "100"
create_option = "Empty"
lun = 0
}
os_profile {
computer_name = "host${count.index + 1}"
admin_username = "${var.admin_username}"
admin_password = "${var.admin_password}"
}
os_profile_windows_config {
}
tags = "${azurestack_resource_group.rg.tags}"
}
provider "azurestack" {
arm_endpoint = "${var.arm_endpoint}"
subscription_id = "${var.subscription_id}"
client_id = "${var.client_id}"
client_secret = "${var.client_secret}"
tenant_id = "${var.tenant_id}"
}
# Create a resource group
resource "azurestack_resource_group" "rg" {
name = "${var.rg_name}"
tags = {
environment = "${var.rg_tag}"
}
}
resource "azurestack_network_security_group" "nsg" {
name = "${azurestack_resource_group.rg.name}-SecurityGroup"
resource_group_name = "${azurestack_resource_group.rg.name}"
security_rule {
name = "ssh"
priority = "100"
direction = "Inbound"
access = "Allow"
protocol = "tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_virtual_network" "virtual-network" {
name = "${azurestack_resource_group.rg.name}-VirtualNetwork"
address_space = ["10.0.0.0/16"]
resource_group_name = "${azurestack_resource_group.rg.name}"
}
resource "azurestack_subnet" "subnet1" {
name = "${azurestack_resource_group.rg.name}-Subnet1"
resource_group_name = "${azurestack_resource_group.rg.name}"
virtual_network_name = "${azurestack_virtual_network.virtual-network.name}"
address_prefix = "10.0.2.0/24"
network_security_group_id = "${azurestack_network_security_group.nsg.id}"
}
resource "azurestack_public_ip" "public-ip" {
count = "${var.vm_count}"
name = "public-ip-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
public_ip_address_al}
resource "azurestack_network_interface" "nic" {
name = "${azurestack_resource_group.rg.name}-NIC${count.index + 1}"
count = "${var.vm_count}"
resource_group_name = "${azurestack_resource_group.rg.name}"
ip_configuration {
name = "nic-ip-config1"
subnet_id = "${azurestack_subnet.subnet1.id}"
private_ip_address_alpublic_ip_address_id = "${element(azurestack_public_ip.public-ip.*.id, count.index)}"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_virtual_machine" "vm" {
count = "${var.vm_count}"
name = "vm-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
network_interface_ids = ["${element(azurestack_network_interface.nic.*.id, count.index)}"]
vm_size = "${var.vm_size}"
# Uncomment this line to delete the OS disk automatically when deleting the VM
# delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM
# delete_data_disks_on_termination = true
storage_image_reference {
publisher = "${element(split("/", var.vm_image_string), 0)}"
offer = "${element(split("/", var.vm_image_string), 1)}"
sku = "${element(split("/", var.vm_image_string), 2)}"
version = "${element(split("/", var.vm_image_string), 3)}"
}
storage_os_disk {
name = "vm-${count.index + 1}-OS-Disk"
caching = "ReadWrite"
managed_disk_type = "Standard_LRS"
create_option = "FromImage"
}
# Optional data disks
storage_data_disk {
name = "vm-${count.index + 1}-Data-Disk"
disk_size_gb = "100"
managed_disk_type = "Standard_LRS"
create_option = "Empty"
lun = 0
}
os_profile {
computer_name = "host${count.index + 1}"
admin_username = "${var.admin_username}"
admin_password = "${var.admin_password}"
}
os_profile_linux_config {
disable_password_authentication = false
}
tags = "${azurestack_resource_group.rg.tags}"
}
provider "azurestack" {
arm_endpoint = "${var.arm_endpoint}"
subscription_id = "${var.subscription_id}"
client_id = "${var.client_id}"
client_secret = "${var.client_secret}"
tenant_id = "${var.tenant_id}"
}
# Create a resource group
resource "azurestack_resource_group" "rg" {
name = "${var.rg_name}"
tags = {
environment = "${var.rg_tag}"
}
}
resource "azurestack_network_security_group" "nsg" {
name = "${azurestack_resource_group.rg.name}-SecurityGroup"
resource_group_name = "${azurestack_resource_group.rg.name}"
security_rule {
name = "ssh"
priority = "100"
direction = "Inbound"
access = "Allow"
protocol = "tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_virtual_network" "virtual-network" {
name = "${azurestack_resource_group.rg.name}-VirtualNetwork"
address_space = ["10.0.0.0/16"]
resource_group_name = "${azurestack_resource_group.rg.name}"
}
resource "azurestack_subnet" "subnet1" {
name = "${azurestack_resource_group.rg.name}-Subnet1"
resource_group_name = "${azurestack_resource_group.rg.name}"
virtual_network_name = "${azurestack_virtual_network.virtual-network.name}"
address_prefix = "10.0.2.0/24"
network_security_group_id = "${azurestack_network_security_group.nsg.id}"
}
resource "azurestack_public_ip" "public-ip" {
count = "${var.vm_count}"
name = "public-ip-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
public_ip_address_al}
resource "azurestack_network_interface" "nic" {
name = "${azurestack_resource_group.rg.name}-NIC${count.index + 1}"
count = "${var.vm_count}"
resource_group_name = "${azurestack_resource_group.rg.name}"
ip_configuration {
name = "nic-ip-config1"
subnet_id = "${azurestack_subnet.subnet1.id}"
private_ip_address_alpublic_ip_address_id = "${element(azurestack_public_ip.public-ip.*.id, count.index)}"
}
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_storage_account" "storage-account" {
count = "${var.vm_count}"
name = "${azurestack_resource_group.rg.name}accsa${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
account_tier = "Standard"
account_replication_type = "LRS"
tags = "${azurestack_resource_group.rg.tags}"
}
resource "azurestack_storage_container" "storage-container" {
count = "${var.vm_count}"
name = "vhds${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
storage_account_name = "${element(azurestack_storage_account.storage-account.*.name, count.index)}"
container_access_type = "private"
}
resource "azurestack_virtual_machine" "vm" {
count = "${var.vm_count}"
name = "vm-${count.index + 1}"
resource_group_name = "${azurestack_resource_group.rg.name}"
network_interface_ids = ["${element(azurestack_network_interface.nic.*.id, count.index)}"]
vm_size = "${var.vm_size}"
# Uncomment this line to delete the OS disk automatically when deleting the VM
# delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM
# delete_data_disks_on_termination = true
storage_image_reference {
publisher = "${element(split("/", var.vm_image_string), 0)}"
offer = "${element(split("/", var.vm_image_string), 1)}"
sku = "${element(split("/", var.vm_image_string), 2)}"
version = "${element(split("/", var.vm_image_string), 3)}"
}
storage_os_disk {
name = "vm-${count.index + 1}-OS-Disk"
vhd_uri = "${element(azurestack_storage_account.storage-account.*.primary_blob_endpoint, count.index)}${element(azurestack_storage_container.storage-container.*.name, count.index)}/vm-${count.index + 1}-osdisk.vhd"
caching = "ReadWrite"
create_option = "FromImage"
}
# Optional data disks
storage_data_disk {
name = "vm-${count.index + 1}-Data-Disk"
vhd_uri = "${element(azurestack_storage_account.storage-account.*.primary_blob_endpoint, count.index)}${element(azurestack_storage_container.storage-container.*.name, count.index)}/vm-${count.index} + 1}-datadisk.vhd"
disk_size_gb = "100"
create_option = "Empty"
lun = 0
}
os_profile {
computer_name = "host${count.index + 1}"
admin_username = "${var.admin_username}"
admin_password = "${var.admin_password}"
}
os_profile_linux_config {
disable_password_authentication = false
}
tags = "${azurestack_resource_group.rg.tags}"
}
Assign values to the variables in the terraform.tfvars
file through the table found at the top of the document
arm_endpoint = ""
subscription_id = ""
client_id = ""
client_secret = ""
tenant_id = ""
vm_count =
vm_image_string = ""
vm_size = ""
rg_name = ""
rg_tag = ""
admin_username = ""
admin_password = ""
Declare the variables here in the variables.tf
file for use in the main script
variable "arm_endpoint" {
type = string
}
variable "subscription_id" {
type = string
}
variable "client_id" {
type = string
}
variable "client_secret" {
type = string
}
variable "tenant_id" {
type = string
}
variable "admin_username" {
type = string
default = "username"
}
variable "admin_password" {
type = string
default = "Password123!"
}
variable "location" {
type = string
}
variable "rg_tag" {
type = string
default = "production"
}
variable "rg_name" {
type = string
}
variable "vm_count" {
default = 1
}
variable "vm_image_string" {
type = string
}
variable "vm_size" {
type = string
default = "Standard_DS2_v2"
}
How to execute a Terraform plan
Tip
Terraform by default scans your execution directory and looks for all tf
files.
From a PowerShell prompt, navigate to the directory that contains your tf
files (including your variable files) and run the following commands:
# Check if your environment is setup correctly
terraform init
Initializing provider plugins...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.azurestack: version = "~> 0.8"
* provider.random: version = "~> 2.1"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "Terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
# Verify your plan
terraform plan -var-file="terraform.tfvars"
# Apply your plan
terraform apply -var-file="terraform.tfvars"
Note
You can also add -auto-approve
to the apply command for it to not ask you to apply changes for full automation.
Feedback
If you find a problem with this article, click Improve this Doc to make the change yourself or raise an issue in GitHub. If you have an idea for how we could improve any of our services, send an email to [email protected].